Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use CopyableText (click-to-copy) in GUI for instance info #3878

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/client/gui/lib/copyable_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart' hide Tooltip;
import 'package:flutter/services.dart';
import 'tooltip.dart';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

class CopyableText extends StatefulWidget {
final String text;
final TextStyle? style;

const CopyableText(this.text, {super.key, this.style});

@override
State<CopyableText> createState() => _CopyableTextState();
}

class _CopyableTextState extends State<CopyableText> {
bool _copied = false;
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
bool get _isCopyable => widget.text != '-';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

void _copyToClipboard() async {
if (!_isCopyable) return;
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
await Clipboard.setData(ClipboardData(text: widget.text));
setState(() => _copied = true);
}

void _resetCopied() {
if (_copied) {
setState(() => _copied = false);
}
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
}

@override
Widget build(BuildContext context) {
Widget text = Text(
widget.text,
style: widget.style,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);

if (!_isCopyable) return text;

return MouseRegion(
cursor: SystemMouseCursors.click,
onExit: (_) => _resetCopied(),
child: GestureDetector(
onTap: _copyToClipboard,
child: Tooltip(
message: _copied ? 'Copied' : 'Click to copy',
child: text,
),
),
);
}
}
30 changes: 26 additions & 4 deletions src/client/gui/lib/tooltip.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart' as fl;

class Tooltip extends fl.StatelessWidget {
class Tooltip extends fl.StatefulWidget {
final fl.Widget child;
final String message;
final bool visible;
Expand All @@ -12,18 +12,40 @@ class Tooltip extends fl.StatelessWidget {
this.visible = true,
});

@override
fl.State<Tooltip> createState() => _TooltipState();
}

class _TooltipState extends fl.State<Tooltip> {
final _key = fl.GlobalKey<fl.TooltipState>();
bool _forceShow = false;

@override
void didUpdateWidget(Tooltip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.message != widget.message) {
setState(() => _forceShow = true);
Future.delayed(const Duration(milliseconds: 1), () {
if (mounted) {
setState(() => _forceShow = false);
}
});
}
}

@override
fl.Widget build(fl.BuildContext context) {
return fl.TooltipVisibility(
visible: visible,
visible: widget.visible,
child: fl.Tooltip(
message: message,
key: _forceShow ? _key : null,
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
message: widget.message,
textAlign: fl.TextAlign.center,
decoration: fl.BoxDecoration(
color: const fl.Color(0xff111111),
borderRadius: fl.BorderRadius.circular(2),
),
child: child,
child: widget.child,
),
);
}
Expand Down
15 changes: 10 additions & 5 deletions src/client/gui/lib/vm_details/ip_addresses.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart' hide Tooltip;
import '../copyable_text.dart';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

import '../extensions.dart';
import '../tooltip.dart';

class IpAddresses extends StatelessWidget {
final Iterable<String> ips;
final bool copyable;

const IpAddresses(this.ips, {super.key});
const IpAddresses(this.ips, {this.copyable = false, super.key});

@override
Widget build(BuildContext context) {
Expand All @@ -15,10 +17,13 @@ class IpAddresses extends StatelessWidget {

return Row(children: [
Expanded(
child: Tooltip(
message: firstIp,
child: Text(firstIp.nonBreaking, overflow: TextOverflow.ellipsis),
),
child: copyable
? CopyableText(firstIp)
: Tooltip(
message: firstIp,
child:
Text(firstIp.nonBreaking, overflow: TextOverflow.ellipsis),
),
),
if (restIps.isNotEmpty)
Badge.count(
Expand Down
17 changes: 9 additions & 8 deletions src/client/gui/lib/vm_details/vm_details_general.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:basics/basics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide Tooltip;
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

import '../extensions.dart';
import '../providers.dart';
Expand All @@ -10,6 +11,8 @@ import 'memory_usage.dart';
import 'vm_action_buttons.dart';
import 'vm_details.dart';
import 'vm_status_icon.dart';
import '../tooltip.dart';
import '../copyable_text.dart';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

class VmDetailsHeader extends ConsumerWidget {
final String name;
Expand Down Expand Up @@ -76,11 +79,9 @@ class VmDetailsHeader extends ConsumerWidget {

final list = [
Expanded(
child: Text(
child: CopyableText(
name.nonBreaking,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w300),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
locationButtons,
Expand Down Expand Up @@ -150,28 +151,28 @@ class GeneralDetails extends ConsumerWidget {
width: 150,
height: baseVmStatHeight,
label: 'IMAGE',
child: Text(info.instanceInfo.currentRelease),
child: CopyableText(info.instanceInfo.currentRelease),
);

final privateIp = VmStat(
width: 150,
height: baseVmStatHeight,
label: 'PRIVATE IP',
child: Text(info.instanceInfo.ipv4.firstOrNull ?? '-'),
child: CopyableText(info.instanceInfo.ipv4.firstOrNull ?? '-'),
);

final publicIp = VmStat(
width: 150,
height: baseVmStatHeight,
label: 'PUBLIC IP',
child: Text(info.instanceInfo.ipv4.skip(1).firstOrNull ?? '-'),
child: CopyableText(info.instanceInfo.ipv4.skip(1).firstOrNull ?? '-'),
);

final created = VmStat(
width: 140,
height: baseVmStatHeight,
label: 'CREATED',
child: Text(
child: CopyableText(
DateFormat('yyyy-MM-dd HH:mm:ss')
.format(info.instanceInfo.creationTimestamp.toDateTime()),
),
Expand Down
14 changes: 10 additions & 4 deletions src/client/gui/lib/vm_table/vm_table_headers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../vm_details/vm_status_icon.dart';
import 'search_box.dart';
import 'table.dart';
import 'vms.dart';
import '../copyable_text.dart';
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved

final headers = <TableHeader<VmInfo>>[
TableHeader(
Expand Down Expand Up @@ -72,23 +73,28 @@ final headers = <TableHeader<VmInfo>>[
minWidth: 70,
cellBuilder: (info) {
final image = info.instanceInfo.currentRelease;
return Text(
return CopyableText(
image.isNotBlank ? image.nonBreaking : '-',
overflow: TextOverflow.ellipsis,
);
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
},
),
TableHeader(
name: 'PRIVATE IP',
width: 140,
minWidth: 100,
cellBuilder: (info) => IpAddresses(info.instanceInfo.ipv4.take(1)),
cellBuilder: (info) => IpAddresses(
info.instanceInfo.ipv4.take(1),
copyable: true,
),
),
TableHeader(
name: 'PUBLIC IP',
width: 140,
minWidth: 100,
cellBuilder: (info) => IpAddresses(info.instanceInfo.ipv4.skip(1)),
cellBuilder: (info) => IpAddresses(
info.instanceInfo.ipv4.skip(1),
copyable: true,
),
andrei-toterman marked this conversation as resolved.
Show resolved Hide resolved
),
];

Expand Down
Loading