Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Syngnat
2026-07-29 12:06:15 +08:00
11 changed files with 628 additions and 88 deletions

View File

@@ -65,8 +65,10 @@ vi.mock('@ant-design/icons', async () => {
const Icon = () => ReactModule.createElement('span', { 'data-icon': true });
return {
DeleteOutlined: Icon,
DownOutlined: Icon,
PlusOutlined: Icon,
ReloadOutlined: Icon,
RightOutlined: Icon,
};
});
@@ -160,7 +162,7 @@ const instanceHealthSwitch = (renderer: ReactTestRenderer, endpoint: string) =>
const instanceAction = (
renderer: ReactTestRenderer,
endpoint: string,
action: 'edit' | 'deregister',
action: 'toggle-details' | 'edit' | 'deregister',
) => instanceRow(renderer, endpoint)?.find(
(node) => node.type === 'button' && node.props['data-instance-action'] === action,
);
@@ -262,6 +264,11 @@ describe('NacosServiceViewer interactions', () => {
enabled: true,
ephemeral: false,
clusterName: 'EDGE',
metadata: {
version: '20260728163719',
slot: 'blue',
activeConnections: '2',
},
}, {
ip: '2001:db8::8',
port: 8848,
@@ -295,6 +302,11 @@ describe('NacosServiceViewer interactions', () => {
const row = instanceRow(renderer!, '10.0.0.8:8848');
expect(row?.props.role).toBe('listitem');
expect(row?.props['data-instance-details-expanded']).toBe('false');
expect(
row?.findByProps({ className: 'gn-nacos-instance-row__main' })
.findAllByProps({ className: 'gn-nacos-instance-row__actions' }),
).toHaveLength(1);
expect(row?.findByProps({ className: 'gn-nacos-instance-row__endpoint' }).children)
.toEqual(['10.0.0.8:8848']);
expect(row?.findAllByType('dt').map((item) => item.children.join(''))).toEqual([
@@ -302,10 +314,43 @@ describe('NacosServiceViewer interactions', () => {
'Weight',
'Type',
]);
expect(
row?.findAllByProps({ className: 'gn-nacos-instance-row__metadata-panel' }),
).toHaveLength(0);
expect(
instanceAction(renderer!, '10.0.0.8:8848', 'toggle-details')?.props['aria-expanded'],
).toBe(false);
await act(async () => {
instanceAction(renderer!, '10.0.0.8:8848', 'toggle-details')?.props.onClick();
instanceAction(renderer!, '[2001:db8::8]:8848', 'toggle-details')?.props.onClick();
});
const expandedRow = instanceRow(renderer!, '10.0.0.8:8848');
expect(expandedRow?.props['data-instance-details-expanded']).toBe('true');
expect(
instanceAction(renderer!, '10.0.0.8:8848', 'toggle-details')?.props['aria-expanded'],
).toBe(true);
expect(
expandedRow?.findByProps({ className: 'gn-nacos-instance-row__metadata-count' }).children,
).toEqual(['3 entries']);
expect(
expandedRow?.findAllByProps({ className: 'gn-nacos-instance-row__metadata-key' })
.map((item) => item.children.join('')),
).toEqual(['activeConnections', 'slot', 'version']);
expect(
expandedRow?.findAllByProps({ className: 'gn-nacos-instance-row__metadata-value' })
.map((item) => item.children.join('')),
).toEqual(['2', 'blue', '20260728163719']);
expect(
instanceRow(renderer!, '[2001:db8::8]:8848')
?.findByProps({ className: 'gn-nacos-instance-row__metadata-empty' }).children,
).toEqual(['This instance has no metadata']);
expect(instanceHealthSwitch(renderer!, '10.0.0.8:8848')?.props['aria-label'])
.toContain('10.0.0.8:8848');
expect(instanceAction(renderer!, '10.0.0.8:8848', 'edit')?.props['aria-label'])
.toContain('10.0.0.8:8848');
expect(instanceAction(renderer!, '10.0.0.8:8848', 'edit')?.props.type).toBe('text');
expect(instanceAction(renderer!, '10.0.0.8:8848', 'deregister')?.props['aria-label'])
.toContain('10.0.0.8:8848');
});

View File

@@ -15,8 +15,10 @@ import {
} from 'antd';
import {
DeleteOutlined,
DownOutlined,
PlusOutlined,
ReloadOutlined,
RightOutlined,
} from '@ant-design/icons';
import RedisResizableDivider from './RedisResizableDivider';
import { buildRedisWorkbenchTheme } from './redisViewerWorkbenchTheme';
@@ -103,6 +105,13 @@ const formatNacosInstanceEndpoint = (ip: string, port: number): string => {
return `${displayHost}:${port}`;
};
const getNacosInstanceMetadataEntries = (
metadata?: Record<string, string>,
): Array<[string, string]> => Object.entries(metadata || {})
.filter(([key]) => key.trim().length > 0)
.map(([key, value]): [string, string] => [key, String(value ?? '')])
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey));
const NACOS_SERVICES_CHANGED_EVENT = 'gonavi:nacos-services-changed';
const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
@@ -176,6 +185,9 @@ const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
const [selectedServiceRaw, setSelectedServiceRaw] = useState<string | null>(null);
const [selectedServiceDetail, setSelectedServiceDetail] = useState<NacosServiceDetail | null>(null);
const [instances, setInstances] = useState<NacosInstance[]>([]);
const [expandedInstanceKeys, setExpandedInstanceKeys] = useState<Set<string>>(
() => new Set(),
);
const [serviceModalOpen, setServiceModalOpen] = useState(false);
const [savingService, setSavingService] = useState(false);
@@ -208,6 +220,22 @@ const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
() => (selectedServiceRaw ? parseNacosServiceName(selectedServiceRaw) : null),
[selectedServiceRaw],
);
useEffect(() => {
setExpandedInstanceKeys(new Set());
}, [connectionId, namespaceId, selectedServiceRaw]);
const toggleInstanceDetails = useCallback((instanceKey: string) => {
setExpandedInstanceKeys((current) => {
const next = new Set(current);
if (next.has(instanceKey)) {
next.delete(instanceKey);
} else {
next.add(instanceKey);
}
return next;
});
}, []);
const serviceRows = useMemo<NacosServiceRow[]>(
() => serviceNames.map((rawName) => ({ rawName, ...parseNacosServiceName(rawName) })),
[serviceNames],
@@ -1024,15 +1052,33 @@ const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
) : null}
{instances.map((instance) => {
const endpoint = formatNacosInstanceEndpoint(instance.ip, instance.port);
const instanceKey = `${endpoint}:${instance.clusterName || ''}`;
const detailsExpanded = expandedInstanceKeys.has(instanceKey);
const metadataEntries = getNacosInstanceMetadataEntries(instance.metadata);
return (
<article
key={`${endpoint}:${instance.clusterName || ''}`}
key={instanceKey}
className="gn-nacos-instance-row"
data-instance-endpoint={endpoint}
data-instance-details-expanded={detailsExpanded ? 'true' : 'false'}
role="listitem"
>
<div className="gn-nacos-instance-row__main">
<div className="gn-nacos-instance-row__identity">
<Button
type="text"
size="small"
className="gn-nacos-instance-row__details-toggle"
icon={detailsExpanded ? <DownOutlined /> : <RightOutlined />}
data-instance-action="toggle-details"
aria-expanded={detailsExpanded}
aria-label={`${tr(
detailsExpanded
? 'nacos_service.action.collapse_instance_details'
: 'nacos_service.action.expand_instance_details',
)} ${endpoint}`}
onClick={() => toggleInstanceDetails(instanceKey)}
/>
<span
className={[
'gn-nacos-instance-row__health-dot',
@@ -1059,6 +1105,37 @@ const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
onChange={(checked) => void handleToggleHealth(instance, checked)}
/>
</div>
<div className="gn-nacos-instance-row__actions">
<Button
type="text"
size="small"
data-instance-action="edit"
aria-label={`${tr('nacos_service.action.edit_instance')} ${endpoint}`}
disabled={dataEditRestricted}
onClick={() => openEditInstance(instance)}
>
{tr('nacos_service.action.edit_instance')}
</Button>
<Popconfirm
title={tr('nacos_service.message.confirm_deregister', {
ip: instance.ip,
port: instance.port,
})}
disabled={dataEditRestricted}
onConfirm={() => void handleDeregister(instance)}
>
<Button
type="text"
size="small"
danger
data-instance-action="deregister"
aria-label={`${tr('nacos_service.action.deregister')} ${endpoint}`}
disabled={dataEditRestricted}
>
{tr('nacos_service.action.deregister')}
</Button>
</Popconfirm>
</div>
<dl className="gn-nacos-instance-row__metadata">
<div>
<dt>{tr('nacos_service.field.cluster')}</dt>
@@ -1077,35 +1154,45 @@ const NacosServiceViewer: React.FC<NacosServiceViewerProps> = ({
</dd>
</div>
</dl>
</div>
<div className="gn-nacos-instance-row__actions">
<Button
size="small"
data-instance-action="edit"
aria-label={`${tr('nacos_service.action.edit_instance')} ${endpoint}`}
disabled={dataEditRestricted}
onClick={() => openEditInstance(instance)}
>
{tr('nacos_service.action.edit_instance')}
</Button>
<Popconfirm
title={tr('nacos_service.message.confirm_deregister', {
ip: instance.ip,
port: instance.port,
})}
disabled={dataEditRestricted}
onConfirm={() => void handleDeregister(instance)}
>
<Button
size="small"
danger
data-instance-action="deregister"
aria-label={`${tr('nacos_service.action.deregister')} ${endpoint}`}
disabled={dataEditRestricted}
{detailsExpanded ? (
<section
className="gn-nacos-instance-row__metadata-panel"
aria-label={tr('nacos_service.field.instance_metadata')}
>
{tr('nacos_service.action.deregister')}
</Button>
</Popconfirm>
<div className="gn-nacos-instance-row__metadata-header">
<span>{tr('nacos_service.field.instance_metadata')}</span>
<span className="gn-nacos-instance-row__metadata-count">
{tr('nacos_service.field.metadata_count', {
count: metadataEntries.length,
})}
</span>
</div>
{metadataEntries.length > 0 ? (
<ul className="gn-nacos-instance-row__metadata-list">
{metadataEntries.map(([key, value]) => (
<li key={key} className="gn-nacos-instance-row__metadata-item">
<span
className="gn-nacos-instance-row__metadata-key"
title={key}
>
{key}
</span>
<span
className="gn-nacos-instance-row__metadata-value"
title={value}
>
{value || '—'}
</span>
</li>
))}
</ul>
) : (
<span className="gn-nacos-instance-row__metadata-empty">
{tr('nacos_service.message.instance_metadata_empty')}
</span>
)}
</section>
) : null}
</div>
</article>
);

View File

@@ -48,6 +48,7 @@ const nacosBackend = vi.hoisted(() => ({
NacosImportConfigs: vi.fn(),
NacosDeleteConfig: vi.fn(),
NacosGetConfig: vi.fn(),
NacosPublishConfig: vi.fn(),
NacosGetBetaConfig: vi.fn(),
NacosStartConfigListen: vi.fn(),
NacosStopConfigListen: vi.fn(),
@@ -245,6 +246,7 @@ describe('NacosViewer config selection actions', () => {
md5: 'test-md5',
},
});
nacosBackend.NacosPublishConfig.mockResolvedValue({ success: true });
nacosBackend.NacosGetBetaConfig.mockResolvedValue({
success: true,
data: { exists: false },
@@ -775,6 +777,18 @@ describe('NacosViewer config selection actions', () => {
expect(antdState.message.info).toHaveBeenCalledTimes(1);
expect(renderedText(renderer!.toJSON())).not.toContain('Listening');
const compactBanner = renderer!.root.find(
(node) => String(node.type) === 'alert',
);
expect(compactBanner.props.className).toBe('gn-v2-nacos-banner');
expect(renderedText(compactBanner)).toContain('Remote config update detected');
expect(renderedText(compactBanner)).toContain('Reload remote');
expect(renderedText(compactBanner)).toContain('Dismiss');
expect(renderedText(compactBanner)).not.toContain('Local draft is clean');
expect(
compactBanner.findByProps({ className: 'gn-v2-nacos-banner__copy' }),
).toBeTruthy();
await act(async () => {
findButtonByExactText(renderer!, 'Reload remote')!.props.onClick();
});
@@ -858,6 +872,135 @@ describe('NacosViewer config selection actions', () => {
renderer!.unmount();
});
it('does not flash an internal dirty badge while publishing edited content', async () => {
const pendingPublish = deferred<any>();
nacosBackend.NacosPublishConfig.mockReturnValue(pendingPublish.promise);
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(
<NacosViewer connectionId="nacos-1" namespaceId="dev" namespaceName="dev" />,
);
});
await flushEffects();
await act(async () => {
latestConfigTableProps().onRow(rows[0]).onClick();
});
await flushEffects();
await act(async () => {
renderer!.root.find(
(node) => (node.type as any) === 'nacos-editor',
).props.onChange('server: changed');
});
const publish = findButtonByExactText(renderer!, 'Publish');
expect(publish?.props.disabled).toBe(false);
expect(renderedText(renderer!.toJSON())).not.toContain('dirty');
act(() => {
publish!.props.onClick();
});
await flushEffects();
expect(renderedText(renderer!.toJSON())).not.toContain('dirty');
pendingPublish.resolve({ success: true });
await flushEffects();
expect(antdState.message.success).toHaveBeenCalledWith('Published successfully');
expect(renderedText(renderer!.toJSON())).not.toContain('dirty');
await act(async () => {
renderer!.unmount();
});
});
it('does not report its own publish event as a remote config change', async () => {
const pendingListReload = deferred<any>();
let pageListCalls = 0;
nacosBackend.NacosSearchConfigs.mockImplementation(
async (_config: unknown, query: any) => {
if (query?.pageSize === 50) {
pageListCalls += 1;
if (pageListCalls === 2) return pendingListReload.promise;
}
return {
success: true,
data: {
totalCount: rows.length,
pageNumber: 1,
pagesAvailable: 1,
pageItems: rows,
},
};
},
);
nacosBackend.NacosStartConfigListen.mockResolvedValue({
success: true,
data: { watchId: 'watch-publish' },
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(
<NacosViewer connectionId="nacos-1" namespaceId="dev" namespaceName="dev" />,
);
});
await flushEffects();
await act(async () => {
latestConfigTableProps().onRow(rows[0]).onClick();
});
await flushEffects();
await act(async () => {
renderer!.root.find(
(node) => (node.type as any) === 'nacos-editor',
).props.onChange('server: published locally');
});
act(() => {
findButtonByExactText(renderer!, 'Publish')!.props.onClick();
});
await flushEffects();
expect(antdState.message.success).toHaveBeenCalledWith('Published successfully');
await act(async () => {
runtimeState.configChangedHandler!({
watchId: 'watch-publish',
connectionId: 'nacos-1',
namespaceId: 'dev',
group: rows[0].group,
dataId: rows[0].dataId,
});
});
await flushEffects();
const infoCallsDuringPublish = antdState.message.info.mock.calls.length;
const remoteBannerDuringPublish = renderedText(renderer!.toJSON()).includes(
'Remote config update detected',
);
pendingListReload.resolve({
success: true,
data: {
totalCount: rows.length,
pageNumber: 1,
pagesAvailable: 1,
pageItems: rows,
},
});
await flushEffects();
await act(async () => {
renderer!.unmount();
});
expect(infoCallsDuringPublish).toBe(0);
expect(remoteBannerDuringPublish).toBe(false);
});
it('disables config import for restrictDataImport protection only', async () => {
storeState.connections[0].config.protection.restrictDataImport = true;

View File

@@ -762,8 +762,12 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
message.warning(tr('nacos_viewer.message.beta_ips_required'));
return;
}
const publishTarget = detail;
const publishSelectionGeneration = selectionGenerationRef.current;
let publishSucceeded = false;
setPublishing(true);
try {
await stopListen();
const res = await (window as any).go.app.App.NacosPublishConfig(rpcConfig, {
namespaceId: namespaceId || '',
dataId: detail.dataId,
@@ -775,9 +779,11 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
betaIps: publishMode === 'beta' ? betaIps.trim() : '',
});
if (!res?.success) {
void startListen(publishTarget, publishSelectionGeneration);
message.error(res?.message || 'publish failed');
return;
}
publishSucceeded = true;
message.success(
publishMode === 'beta'
? tr('nacos_viewer.message.beta_publish_success')
@@ -787,12 +793,15 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
setRemoteChanged(false);
await loadList(pageNo);
await loadDetail({
dataId: detail.dataId,
group: detail.group,
dataId: publishTarget.dataId,
group: publishTarget.group,
type: draftType,
});
await loadBetaMeta({ dataId: detail.dataId, group: detail.group });
await loadBetaMeta({ dataId: publishTarget.dataId, group: publishTarget.group });
} catch (error: any) {
if (!publishSucceeded) {
void startListen(publishTarget, publishSelectionGeneration);
}
message.error(error?.message || String(error));
} finally {
setPublishing(false);
@@ -1245,6 +1254,9 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
];
const namespaceLabel = namespaceName || (namespaceId ? namespaceId : 'public');
const remoteChangedHint = draftDirty
? tr('nacos_viewer.message.remote_changed_dirty_hint')
: tr('nacos_viewer.message.remote_changed_clean_hint');
return (
<div
@@ -1266,19 +1278,33 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
type="warning"
showIcon
className={isV2Ui ? 'gn-v2-nacos-banner' : undefined}
style={isV2Ui ? { margin: 0, borderRadius: 0 } : undefined}
message={tr('nacos_viewer.message.remote_changed_banner')}
description={
draftDirty
? tr('nacos_viewer.message.remote_changed_dirty_hint')
: tr('nacos_viewer.message.remote_changed_clean_hint')
message={
isV2Ui ? (
<span className="gn-v2-nacos-banner__copy">
<span className="gn-v2-nacos-banner__title">
{tr('nacos_viewer.message.remote_changed_banner')}
</span>
{draftDirty ? (
<span className="gn-v2-nacos-banner__hint" title={remoteChangedHint}>
{remoteChangedHint}
</span>
) : null}
</span>
) : (
tr('nacos_viewer.message.remote_changed_banner')
)
}
description={isV2Ui ? undefined : remoteChangedHint}
action={
<Space>
<Space size={4}>
<Button size="small" type="primary" onClick={() => void handleReloadRemote()}>
{tr('nacos_viewer.action.reload_remote')}
</Button>
<Button size="small" onClick={() => setRemoteChanged(false)}>
<Button
size="small"
type={isV2Ui ? 'text' : 'default'}
onClick={() => setRemoteChanged(false)}
>
{tr('nacos_viewer.action.dismiss_remote')}
</Button>
</Space>
@@ -1608,7 +1634,6 @@ const NacosViewer: React.FC<NacosViewerProps> = ({
<Tag>{detail.group}</Tag>
<strong style={{ color: workbenchTheme.textPrimary }}>{detail.dataId}</strong>
{detail.md5 ? <Tag color="default">{detail.md5}</Tag> : null}
{draftDirty ? <Tag color="orange">dirty</Tag> : null}
{betaExists ? <Tag color="purple">{tr('nacos_viewer.status.beta_active')}</Tag> : null}
</>
) : (

View File

@@ -908,10 +908,83 @@ body[data-ui-version="v2"] .gn-v2-nacos-workbench {
}
body[data-ui-version="v2"] .gn-v2-nacos-banner {
border-radius: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
border-top: 0 !important;
flex: 0 0 auto;
align-self: center;
width: min(820px, calc(100% - 24px));
min-height: 40px;
margin: 8px auto !important;
padding: 6px 8px 6px 10px !important;
align-items: center !important;
border: 1px solid color-mix(in srgb, var(--gn-warn) 28%, transparent) !important;
border-inline-start: 3px solid var(--gn-warn) !important;
border-radius: 8px !important;
background: color-mix(in srgb, var(--gn-warn-soft) 72%, var(--gn-bg-panel)) !important;
box-shadow: none !important;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner .ant-alert-icon {
margin-inline-end: 9px;
color: var(--gn-warn) !important;
font-size: 14px;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner .ant-alert-content {
min-width: 0;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner .ant-alert-message {
margin-bottom: 0;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner__copy {
min-width: 0;
display: flex;
align-items: baseline;
gap: 8px;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner__title {
flex: 0 0 auto;
color: var(--gn-fg-1);
font-size: 12px;
font-weight: 650;
line-height: 1.4;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner__hint {
min-width: 0;
overflow: hidden;
color: var(--gn-fg-3);
font-size: 11px;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner .ant-alert-action {
margin-inline-start: 12px;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner .ant-btn {
height: 26px;
padding-inline: 9px;
}
@media (max-width: 720px) {
body[data-ui-version="v2"] .gn-v2-nacos-banner {
width: calc(100% - 12px);
margin: 6px !important;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner__copy {
align-items: flex-start;
flex-direction: column;
gap: 1px;
}
body[data-ui-version="v2"] .gn-v2-nacos-banner__hint {
white-space: normal;
}
}
/*
@@ -1459,28 +1532,27 @@ body[data-ui-version="v2"] .gn-v2-nacos-detail-pane .ant-table-tbody > tr.ant-ta
.gn-nacos-instance-row {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: 20px;
min-width: 0;
padding: 16px 4px 16px 8px;
border-bottom: 1px solid var(--gn-br-1, rgba(15, 23, 42, 0.08));
padding: 0 8px;
}
.gn-nacos-instance-row:first-child {
border-top: 1px solid var(--gn-br-1, rgba(15, 23, 42, 0.08));
.gn-nacos-instance-row + .gn-nacos-instance-row {
margin-top: 4px;
}
.gn-nacos-instance-row__main {
min-width: 0;
display: grid;
grid-template-columns: minmax(180px, 1fr) auto;
align-items: center;
gap: 13px 20px;
grid-template-columns: minmax(240px, 1.2fr) minmax(300px, 1fr) auto auto;
align-items: stretch;
}
.gn-nacos-instance-row__identity {
grid-column: 1;
grid-row: 1;
min-width: 0;
min-height: 52px;
padding: 9px 10px;
display: flex;
align-items: center;
gap: 9px;
@@ -1491,6 +1563,20 @@ body[data-ui-version="v2"] .gn-v2-nacos-detail-pane .ant-table-tbody > tr.ant-ta
margin: 0;
}
.gn-nacos-instance-row__details-toggle.ant-btn {
width: 24px;
min-width: 24px;
height: 24px;
flex: 0 0 24px;
margin-inline-start: -4px;
padding: 0;
color: var(--gn-fg-4, inherit);
}
.gn-nacos-instance-row__details-toggle.ant-btn .anticon {
font-size: 10px;
}
.gn-nacos-instance-row__health-dot {
width: 7px;
height: 7px;
@@ -1522,6 +1608,10 @@ body[data-ui-version="v2"] .gn-v2-nacos-detail-pane .ant-table-tbody > tr.ant-ta
}
.gn-nacos-instance-row__health-control {
grid-column: 3;
grid-row: 1;
min-height: 52px;
padding: 9px 12px;
display: inline-flex;
align-items: center;
justify-content: flex-end;
@@ -1532,20 +1622,21 @@ body[data-ui-version="v2"] .gn-v2-nacos-detail-pane .ant-table-tbody > tr.ant-ta
}
.gn-nacos-instance-row__metadata {
grid-column: 1 / -1;
grid-column: 2;
grid-row: 1;
min-width: 0;
margin: 0;
padding: 7px 14px;
display: grid;
grid-template-columns: repeat(3, minmax(88px, 1fr));
gap: 8px 18px;
grid-template-columns: repeat(3, minmax(72px, 1fr));
align-items: center;
gap: 12px;
}
.gn-nacos-instance-row__metadata > div {
min-width: 0;
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
align-items: baseline;
gap: 7px;
gap: 1px;
}
.gn-nacos-instance-row__metadata dt {
@@ -1566,17 +1657,102 @@ body[data-ui-version="v2"] .gn-v2-nacos-detail-pane .ant-table-tbody > tr.ant-ta
white-space: nowrap;
}
.gn-nacos-instance-row__actions {
align-self: stretch;
display: flex;
align-items: center;
gap: 8px;
padding-inline-start: 16px;
border-inline-start: 1px solid var(--gn-br-1, rgba(15, 23, 42, 0.08));
.gn-nacos-instance-row__metadata-panel {
grid-column: 1 / -1;
min-width: 0;
overflow: hidden;
}
body[data-ui-version="v2"] .gn-nacos-instance-row:hover {
background: color-mix(in srgb, var(--gn-bg-hover) 72%, transparent);
.gn-nacos-instance-row__metadata-header {
min-height: 30px;
padding: 6px 10px 6px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--gn-br-1, rgba(15, 23, 42, 0.08));
color: var(--gn-fg-3, inherit);
font-size: 11px;
font-weight: 600;
line-height: 1.4;
}
.gn-nacos-instance-row__metadata-count {
flex: 0 0 auto;
color: var(--gn-fg-4, inherit);
font-family: var(--gn-font-mono, ui-monospace, monospace);
font-size: 10px;
font-weight: 500;
}
.gn-nacos-instance-row__metadata-list {
margin: 0;
padding: 0;
display: grid;
grid-template-columns:
minmax(150px, 0.52fr) minmax(170px, 1fr)
minmax(150px, 0.52fr) minmax(170px, 1fr);
list-style: none;
}
.gn-nacos-instance-row__metadata-item {
display: contents;
}
.gn-nacos-instance-row__metadata-key,
.gn-nacos-instance-row__metadata-value {
min-width: 0;
min-height: 36px;
padding: 8px 10px;
display: flex;
align-items: flex-start;
border-bottom: 1px solid color-mix(in srgb, var(--gn-br-1, rgba(15, 23, 42, 0.08)) 72%, transparent);
font-family: var(--gn-font-mono, ui-monospace, monospace);
font-size: 11px;
line-height: 1.45;
}
.gn-nacos-instance-row__metadata-key {
overflow: hidden;
color: var(--gn-fg-4, inherit);
background: color-mix(in srgb, var(--gn-bg-panel, transparent) 48%, transparent);
border-inline-end: 1px solid color-mix(in srgb, var(--gn-br-1, rgba(15, 23, 42, 0.08)) 72%, transparent);
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-nacos-instance-row__metadata-item:nth-child(even) .gn-nacos-instance-row__metadata-key {
border-inline-start: 1px solid color-mix(in srgb, var(--gn-br-1, rgba(15, 23, 42, 0.08)) 72%, transparent);
}
.gn-nacos-instance-row__metadata-value {
color: var(--gn-fg-2, inherit);
font-weight: 550;
overflow-wrap: anywhere;
}
.gn-nacos-instance-row__metadata-empty {
min-height: 42px;
padding: 10px;
display: flex;
align-items: center;
color: var(--gn-fg-4, inherit);
font-size: 11px;
}
.gn-nacos-instance-row__actions {
grid-column: 4;
grid-row: 1;
min-height: 52px;
padding: 8px 4px 8px 8px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2px;
}
.gn-nacos-instance-row__actions .ant-btn {
padding-inline: 8px;
}
body[data-ui-version="v2"] .gn-nacos-service-table .ant-table-tbody > tr {
@@ -1587,32 +1763,66 @@ body[data-ui-version="v2"] .gn-nacos-service-list-footer {
color: var(--gn-fg-4);
}
@container gn-nacos-instance-inspector (max-width: 620px) {
.gn-nacos-instance-row {
grid-template-columns: minmax(0, 1fr);
row-gap: 14px;
@container gn-nacos-instance-inspector (max-width: 860px) {
.gn-nacos-instance-row__main {
grid-template-columns: minmax(0, 1fr) auto auto;
}
.gn-nacos-instance-row__metadata {
grid-column: 1 / -1;
grid-row: 2;
}
.gn-nacos-instance-row__health-control {
grid-column: 2;
}
.gn-nacos-instance-row__actions {
min-height: 34px;
justify-content: flex-end;
padding: 12px 0 0;
grid-column: 3;
}
}
@container gn-nacos-instance-inspector (max-width: 760px) {
.gn-nacos-instance-row__metadata-list {
grid-template-columns: minmax(128px, 0.42fr) minmax(0, 1fr);
}
.gn-nacos-instance-row__metadata-item:nth-child(even) .gn-nacos-instance-row__metadata-key {
border-inline-start: 0;
border-top: 1px solid var(--gn-br-1, rgba(15, 23, 42, 0.08));
}
}
@container gn-nacos-instance-inspector (max-width: 620px) {
.gn-nacos-instance-row__main {
grid-template-columns: minmax(0, 1fr) auto;
}
.gn-nacos-instance-row__identity {
grid-column: 1 / -1;
}
.gn-nacos-instance-row__metadata {
grid-row: 2;
}
.gn-nacos-instance-row__health-control {
grid-column: 1;
grid-row: 3;
min-height: 42px;
justify-content: flex-start;
}
.gn-nacos-instance-row__actions {
grid-column: 2;
grid-row: 3;
min-height: 42px;
justify-content: flex-end;
}
}
@container gn-nacos-instance-inspector (max-width: 440px) {
.gn-nacos-instance-row__main {
grid-template-columns: minmax(0, 1fr);
}
.gn-nacos-instance-row__health-control {
justify-content: flex-start;
}
.gn-nacos-instance-row__metadata {
grid-template-columns: minmax(0, 1fr);
grid-template-columns: repeat(3, minmax(64px, 1fr));
}
}