|
| 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 | +}; |
0 commit comments