Skip to content

Commit 34ac83a

Browse files
committed
Add compare view with diff selector, read-only aliases, and UI cleanups
Signed-off-by: Nana Nosirova <10577112+nananosirova@users.noreply.github.com>
1 parent 39c1815 commit 34ac83a

7 files changed

Lines changed: 646 additions & 61 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { useMemo } from 'react';
2+
import {
3+
Button,
4+
ExpandMoreIcon,
5+
Spacer,
6+
Tag,
7+
Tooltip,
8+
Typography,
9+
useDesignSystemTheme,
10+
} from '@databricks/design-system';
11+
import { FormattedMessage, useIntl } from 'react-intl';
12+
import { diffWords } from '../../experiment-tracking/pages/prompts/diff';
13+
14+
import type { TagColors } from '@databricks/design-system';
15+
import type { MCPServerVersion } from '../types';
16+
import { STATUS_TAG_COLOR } from '../utils';
17+
import { AliasTag } from '../../common/components/AliasTag';
18+
import { KeyValueTag } from '../../common/components/KeyValueTag';
19+
import Utils from '../../common/utils/Utils';
20+
21+
const VersionMetadataGrid = ({
22+
version,
23+
serverName,
24+
aliasesByVersion,
25+
aliasColors,
26+
}: {
27+
version?: MCPServerVersion;
28+
serverName: string;
29+
aliasesByVersion: Record<string, string[]>;
30+
aliasColors?: Record<string, TagColors>;
31+
}) => {
32+
const { theme } = useDesignSystemTheme();
33+
const intl = useIntl();
34+
35+
if (!version) return null;
36+
37+
return (
38+
<div
39+
css={{
40+
display: 'grid',
41+
gridTemplateColumns: '100px 1fr',
42+
gridAutoRows: `minmax(${theme.typography.lineHeightLg}, auto)`,
43+
alignItems: 'flex-start',
44+
rowGap: theme.spacing.xs,
45+
columnGap: theme.spacing.sm,
46+
}}
47+
>
48+
<Typography.Text bold>
49+
<FormattedMessage defaultMessage="Status:" description="MCP compare metadata status label" />
50+
</Typography.Text>
51+
<span>
52+
<Tag componentId="mlflow.mcp_registry.compare.status" color={STATUS_TAG_COLOR[version.status]}>
53+
{version.status}
54+
</Tag>
55+
</span>
56+
57+
<Typography.Text bold>
58+
<FormattedMessage defaultMessage="Aliases:" description="MCP compare metadata aliases label" />
59+
</Typography.Text>
60+
<div css={{ display: 'flex', flexWrap: 'wrap', gap: theme.spacing.xs }}>
61+
{(aliasesByVersion[version.version] ?? []).length > 0 ? (
62+
(aliasesByVersion[version.version] ?? []).map((alias) => (
63+
<AliasTag key={alias} value={alias} color={aliasColors?.[alias]} />
64+
))
65+
) : (
66+
<Typography.Hint></Typography.Hint>
67+
)}
68+
</div>
69+
70+
<Typography.Text bold>
71+
<FormattedMessage defaultMessage="Created:" description="MCP compare metadata created label" />
72+
</Typography.Text>
73+
<Typography.Text>
74+
{version.creation_timestamp ? Utils.formatTimestamp(version.creation_timestamp, intl) : '—'}
75+
</Typography.Text>
76+
77+
{Object.keys(version.tags).length > 0 && (
78+
<>
79+
<Typography.Text bold>
80+
<FormattedMessage defaultMessage="Metadata:" description="MCP compare metadata tags label" />
81+
</Typography.Text>
82+
<div css={{ display: 'flex', flexWrap: 'wrap', gap: theme.spacing.xs }}>
83+
{Object.entries(version.tags).map(([key, value]) => (
84+
<KeyValueTag css={{ margin: 0 }} key={key} tag={{ key, value }} />
85+
))}
86+
</div>
87+
</>
88+
)}
89+
</div>
90+
);
91+
};
92+
93+
export const MCPServerVersionCompare = ({
94+
baselineVersion,
95+
comparedVersion,
96+
serverName,
97+
aliasesByVersion,
98+
aliasColors,
99+
onSwitchSides,
100+
}: {
101+
baselineVersion?: MCPServerVersion;
102+
comparedVersion?: MCPServerVersion;
103+
serverName: string;
104+
aliasesByVersion: Record<string, string[]>;
105+
aliasColors?: Record<string, TagColors>;
106+
onSwitchSides: () => void;
107+
}) => {
108+
const { theme } = useDesignSystemTheme();
109+
const intl = useIntl();
110+
111+
const baselineJson = useMemo(
112+
() => (baselineVersion?.server_json ? JSON.stringify(baselineVersion.server_json, null, 2) : ''),
113+
[baselineVersion],
114+
);
115+
const comparedJson = useMemo(
116+
() => (comparedVersion?.server_json ? JSON.stringify(comparedVersion.server_json, null, 2) : ''),
117+
[comparedVersion],
118+
);
119+
120+
const diff = useMemo(() => diffWords(baselineJson, comparedJson) ?? [], [baselineJson, comparedJson]);
121+
122+
const colors = useMemo(
123+
() => ({
124+
addedBackground: theme.isDarkMode ? theme.colors.green700 : theme.colors.green300,
125+
removedBackground: theme.isDarkMode ? theme.colors.red700 : theme.colors.red300,
126+
}),
127+
[theme],
128+
);
129+
130+
return (
131+
<div
132+
css={{
133+
flex: 1,
134+
padding: theme.spacing.md,
135+
paddingTop: 0,
136+
overflow: 'hidden',
137+
display: 'flex',
138+
flexDirection: 'column',
139+
}}
140+
>
141+
<Typography.Title level={3}>
142+
<FormattedMessage
143+
defaultMessage="Comparing version {baseline} with version {compared}"
144+
description="MCP server version compare heading"
145+
values={{
146+
baseline: baselineVersion?.version,
147+
compared: comparedVersion?.version,
148+
}}
149+
/>
150+
</Typography.Title>
151+
152+
<div css={{ display: 'flex' }}>
153+
<div css={{ flex: 1 }}>
154+
<VersionMetadataGrid
155+
version={baselineVersion}
156+
serverName={serverName}
157+
aliasesByVersion={aliasesByVersion}
158+
aliasColors={aliasColors}
159+
/>
160+
</div>
161+
<div css={{ paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm }}>
162+
<div css={{ width: theme.general.heightSm }} />
163+
</div>
164+
<div css={{ flex: 1 }}>
165+
<VersionMetadataGrid
166+
version={comparedVersion}
167+
serverName={serverName}
168+
aliasesByVersion={aliasesByVersion}
169+
aliasColors={aliasColors}
170+
/>
171+
</div>
172+
</div>
173+
174+
<Spacer shrinks={false} />
175+
176+
<div css={{ display: 'flex', flex: 1, overflow: 'auto', alignItems: 'flex-start' }}>
177+
<pre
178+
css={{
179+
flex: 1,
180+
margin: 0,
181+
padding: theme.spacing.md,
182+
backgroundColor: theme.colors.backgroundSecondary,
183+
borderRadius: theme.borders.borderRadiusSm,
184+
overflow: 'auto',
185+
fontSize: theme.typography.fontSizeSm,
186+
whiteSpace: 'pre-wrap',
187+
wordBreak: 'break-word',
188+
}}
189+
>
190+
<code>{baselineJson || 'Empty'}</code>
191+
</pre>
192+
193+
<div css={{ paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm }}>
194+
<Tooltip
195+
componentId="mlflow.mcp_registry.compare.switch_sides.tooltip"
196+
content={
197+
<FormattedMessage
198+
defaultMessage="Switch sides"
199+
description="Label for button to switch MCP server versions in comparison view"
200+
/>
201+
}
202+
side="top"
203+
>
204+
<Button
205+
aria-label={intl.formatMessage({
206+
defaultMessage: 'Switch sides',
207+
description: 'Label for button to switch MCP server versions in comparison view',
208+
})}
209+
componentId="mlflow.mcp_registry.compare.switch_sides"
210+
icon={<ExpandMoreIcon css={{ svg: { rotate: '90deg' } }} />}
211+
onClick={onSwitchSides}
212+
/>
213+
</Tooltip>
214+
</div>
215+
216+
<pre
217+
css={{
218+
flex: 1,
219+
margin: 0,
220+
padding: theme.spacing.md,
221+
backgroundColor: theme.colors.backgroundSecondary,
222+
borderRadius: theme.borders.borderRadiusSm,
223+
overflow: 'auto',
224+
fontSize: theme.typography.fontSizeSm,
225+
whiteSpace: 'pre-wrap',
226+
wordBreak: 'break-word',
227+
}}
228+
>
229+
<code>
230+
{diff.map((part, index) => (
231+
<span
232+
key={index}
233+
css={{
234+
backgroundColor: part.added
235+
? colors.addedBackground
236+
: part.removed
237+
? colors.removedBackground
238+
: undefined,
239+
textDecoration: part.removed ? 'line-through' : 'none',
240+
}}
241+
>
242+
{part.value}
243+
</span>
244+
))}
245+
</code>
246+
</pre>
247+
</div>
248+
</div>
249+
);
250+
};

mlflow/server/js/src/mcp-registry/components/MCPServerVersionDetail.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,6 @@ export const MCPServerVersionDetail = ({
284284
/>
285285
</span>
286286

287-
<Typography.Text bold>
288-
<FormattedMessage
289-
defaultMessage="Server version:"
290-
description="MCP server version detail server version label"
291-
/>
292-
</Typography.Text>
293-
<Typography.Text>{version.server_json?.version || version.version}</Typography.Text>
294-
295287
{version.server_json?.websiteUrl && (
296288
<>
297289
<Typography.Text bold>
@@ -324,7 +316,7 @@ export const MCPServerVersionDetail = ({
324316

325317
<Typography.Text bold>
326318
<FormattedMessage
327-
defaultMessage="Registered at:"
319+
defaultMessage="Created:"
328320
description="MCP server version detail registered at label"
329321
/>
330322
</Typography.Text>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Tooltip, useDesignSystemTheme } from '@databricks/design-system';
2+
import { FormattedMessage, useIntl } from 'react-intl';
3+
4+
export const MCPServerVersionDiffSelectorButton = ({
5+
isSelectedBaseline,
6+
isSelectedCompared,
7+
onSelectBaseline,
8+
onSelectCompared,
9+
}: {
10+
isSelectedBaseline: boolean;
11+
isSelectedCompared: boolean;
12+
onSelectBaseline?: () => void;
13+
onSelectCompared?: () => void;
14+
}) => {
15+
const { theme } = useDesignSystemTheme();
16+
const intl = useIntl();
17+
return (
18+
<div
19+
css={{ width: theme.general.buttonHeight, display: 'flex', alignItems: 'center', paddingRight: theme.spacing.sm }}
20+
>
21+
<div css={{ display: 'flex', height: theme.general.buttonInnerHeight + theme.spacing.xs, gap: 0, flex: 1 }}>
22+
<Tooltip
23+
componentId="mlflow.mcp_registry.detail.select_baseline.tooltip"
24+
content={
25+
<FormattedMessage
26+
defaultMessage="Select as baseline version"
27+
description="Label for selecting baseline MCP server version in the comparison view"
28+
/>
29+
}
30+
delayDuration={0}
31+
side="left"
32+
>
33+
<button
34+
onClick={onSelectBaseline}
35+
role="radio"
36+
aria-checked={isSelectedBaseline}
37+
aria-label={intl.formatMessage({
38+
defaultMessage: 'Select as baseline version',
39+
description: 'Label for selecting baseline MCP server version in the comparison view',
40+
})}
41+
css={{
42+
flex: 1,
43+
border: `1px solid ${
44+
isSelectedBaseline ? theme.colors.actionDefaultBorderFocus : theme.colors.actionDefaultBorderDefault
45+
}`,
46+
borderRight: 0,
47+
marginLeft: 1,
48+
borderTopLeftRadius: theme.borders.borderRadiusMd,
49+
borderBottomLeftRadius: theme.borders.borderRadiusMd,
50+
backgroundColor: isSelectedBaseline
51+
? theme.colors.actionDefaultBackgroundPress
52+
: theme.colors.actionDefaultBackgroundDefault,
53+
cursor: 'pointer',
54+
'&:hover': { backgroundColor: theme.colors.actionDefaultBackgroundHover },
55+
}}
56+
/>
57+
</Tooltip>
58+
<Tooltip
59+
componentId="mlflow.mcp_registry.detail.select_compared.tooltip"
60+
content={
61+
<FormattedMessage
62+
defaultMessage="Select as compared version"
63+
description="Label for selecting compared MCP server version in the comparison view"
64+
/>
65+
}
66+
delayDuration={0}
67+
side="right"
68+
>
69+
<button
70+
onClick={onSelectCompared}
71+
role="radio"
72+
aria-checked={isSelectedCompared}
73+
aria-label={intl.formatMessage({
74+
defaultMessage: 'Select as compared version',
75+
description: 'Label for selecting compared MCP server version in the comparison view',
76+
})}
77+
css={{
78+
flex: 1,
79+
border: `1px solid ${
80+
isSelectedCompared ? theme.colors.actionDefaultBorderFocus : theme.colors.actionDefaultBorderDefault
81+
}`,
82+
borderLeft: `1px solid ${
83+
isSelectedBaseline || isSelectedCompared
84+
? theme.colors.actionDefaultBorderFocus
85+
: theme.colors.actionDefaultBorderDefault
86+
}`,
87+
borderTopRightRadius: theme.borders.borderRadiusMd,
88+
borderBottomRightRadius: theme.borders.borderRadiusMd,
89+
backgroundColor: isSelectedCompared
90+
? theme.colors.actionDefaultBackgroundPress
91+
: theme.colors.actionDefaultBackgroundDefault,
92+
cursor: 'pointer',
93+
'&:hover': { backgroundColor: theme.colors.actionDefaultBackgroundHover },
94+
}}
95+
/>
96+
</Tooltip>
97+
</div>
98+
</div>
99+
);
100+
};

0 commit comments

Comments
 (0)