Skip to content

Commit 9c84772

Browse files
committed
feat(schema-registry): improve schema metadata UI
- Add Properties subheading under Metadata section - Display metadata properties in a table format - Load existing metadata when adding new schema version - Fix version selection to show latest after creating new version - Invalidate React Query cache after schema creation
1 parent adc1afb commit 9c84772

File tree

2 files changed

+143
-6
lines changed

2 files changed

+143
-6
lines changed

frontend/src/components/pages/schemas/schema-create.tsx

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
IconButton,
2424
Input,
2525
RadioGroup,
26+
Text,
2627
useToast,
2728
} from '@redpanda-data/ui';
29+
import { useQueryClient } from '@tanstack/react-query';
2830
import { TrashIcon } from 'components/icons';
2931
import { InfoIcon } from 'lucide-react';
3032
import { observable } from 'mobx';
@@ -150,6 +152,16 @@ export class SchemaAddVersionPage extends PageComponent<{ subjectName: string }>
150152
this.editorState.references = schema.references;
151153
this.editorState.strategy = 'CUSTOM';
152154
this.editorState.userInput = subject.name;
155+
156+
// Load existing metadata properties for editing
157+
if (schema.metadata?.properties) {
158+
this.editorState.metadataProperties = Object.entries(schema.metadata.properties).map(([key, value]) => ({
159+
key,
160+
value,
161+
}));
162+
// Add an empty row for adding new properties
163+
this.editorState.metadataProperties.push({ key: '', value: '' });
164+
}
153165
}
154166

155167
return (
@@ -176,6 +188,7 @@ const SchemaPageButtons = observer(
176188
editorState: SchemaEditorStateHelper;
177189
}) => {
178190
const toast = useToast();
191+
const queryClient = useQueryClient();
179192
const [isValidating, setValidating] = useState(false);
180193
const [isCreating, setCreating] = useState(false);
181194
const [persistentValidationError, setPersistentValidationError] = useState<{
@@ -248,6 +261,7 @@ const SchemaPageButtons = observer(
248261
schemaType: editorState.format as SchemaTypeType,
249262
schema: editorState.schemaText,
250263
references: editorState.references.filter((x) => x.name && x.subject),
264+
metadata: editorState.computedMetadata,
251265
params: {
252266
normalize: editorState.normalize,
253267
},
@@ -256,15 +270,17 @@ const SchemaPageButtons = observer(
256270

257271
await api.refreshSchemaDetails(subjectName, true);
258272

259-
// success: navigate to details
260-
const latestVersion = api.schemaDetails.get(subjectName)?.latestActiveVersion;
273+
// Invalidate React Query cache so details page shows latest data
274+
await queryClient.invalidateQueries({
275+
queryKey: ['schemaRegistry', 'subjects', subjectName, 'details'],
276+
});
277+
278+
// success: navigate to details with "latest" so it picks up the new version
261279
// biome-ignore lint/suspicious/noConsole: intentional console usage
262280
console.log('schema created', { response: r });
263281
// biome-ignore lint/suspicious/noConsole: intentional console usage
264-
console.log('navigating to details', { subjectName, latestVersion });
265-
appGlobal.historyReplace(
266-
`/schema-registry/subjects/${encodeURIComponent(subjectName)}?version=${latestVersion}`
267-
);
282+
console.log('navigating to details', { subjectName });
283+
appGlobal.historyReplace(`/schema-registry/subjects/${encodeURIComponent(subjectName)}?version=latest`);
268284
} catch (err) {
269285
// error: open modal
270286
// biome-ignore lint/suspicious/noConsole: intentional console usage
@@ -547,6 +563,13 @@ const SchemaEditor = observer((p: { state: SchemaEditorStateHelper; mode: 'CREAT
547563
{/* <Text>This is an example help text about the references list, to be updated later</Text> */}
548564

549565
<ReferencesEditor state={state} />
566+
567+
<Heading mt="8" variant="lg">
568+
Schema metadata
569+
</Heading>
570+
<Text>Optional key-value properties to associate with this schema.</Text>
571+
572+
<MetadataPropertiesEditor state={state} />
550573
</Flex>
551574
</>
552575
);
@@ -636,6 +659,59 @@ const ReferencesEditor = observer((p: { state: SchemaEditorStateHelper }) => {
636659
);
637660
});
638661

662+
const MetadataPropertiesEditor = observer((p: { state: SchemaEditorStateHelper }) => {
663+
const { state } = p;
664+
const props = state.metadataProperties;
665+
666+
const renderRow = (prop: { key: string; value: string }, index: number) => (
667+
<Flex alignItems="flex-end" gap="4" key={index}>
668+
<FormField label="Key">
669+
<Input
670+
data-testid={`schema-create-metadata-key-input-${index}`}
671+
onChange={(e) => {
672+
prop.key = e.target.value;
673+
}}
674+
placeholder="e.g. owner"
675+
value={prop.key}
676+
/>
677+
</FormField>
678+
<FormField label="Value">
679+
<Input
680+
data-testid={`schema-create-metadata-value-input-${index}`}
681+
onChange={(e) => {
682+
prop.value = e.target.value;
683+
}}
684+
placeholder="e.g. team-platform"
685+
value={prop.value}
686+
/>
687+
</FormField>
688+
<IconButton
689+
aria-label="delete"
690+
data-testid={`schema-create-metadata-delete-btn-${index}`}
691+
icon={<TrashIcon fontSize="19px" />}
692+
onClick={() => props.remove(prop)}
693+
variant="ghost"
694+
/>
695+
</Flex>
696+
);
697+
698+
return (
699+
<Flex direction="column" gap="4">
700+
{props.map((x, index) => renderRow(x, index))}
701+
702+
<Button
703+
data-testid="schema-create-add-metadata-btn"
704+
onClick={() => props.push({ key: '', value: '' })}
705+
size="sm"
706+
variant="outline"
707+
width="fit-content"
708+
>
709+
Add property
710+
</Button>
711+
</Flex>
712+
);
713+
});
714+
639715
function createSchemaState() {
640716
return observable({
641717
strategy: 'TOPIC' as
@@ -654,6 +730,17 @@ function createSchemaState() {
654730
version: number;
655731
}[],
656732
normalize: false,
733+
metadataProperties: [{ key: '', value: '' }] as { key: string; value: string }[],
734+
735+
get computedMetadata(): { properties: Record<string, string> } | undefined {
736+
const properties: Record<string, string> = {};
737+
for (const prop of this.metadataProperties) {
738+
if (prop.key && prop.value) {
739+
properties[prop.key] = prop.value;
740+
}
741+
}
742+
return Object.keys(properties).length > 0 ? { properties } : undefined;
743+
},
657744

658745
get isInvalidKeyOrValue() {
659746
return this.strategy === 'TOPIC' && this.userInput.length > 0 && !this.keyOrValue;

frontend/src/components/pages/schemas/schema-details.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from '@redpanda-data/ui';
3030
import { useQueryClient } from '@tanstack/react-query';
3131
import { getRouteApi, Link, useNavigate } from '@tanstack/react-router';
32+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'components/redpanda-ui/components/table';
3233

3334
const routeApi = getRouteApi('/schema-registry/subjects/$subjectName/');
3435

@@ -307,6 +308,13 @@ const SubjectDefinition = (p: { subject: SchemaRegistrySubjectDetails }) => {
307308
}
308309
}, [versionNumber, requestedVersionExists, fallbackVersion, toast, navigate]);
309310

311+
// When URL is "latest", sync selectedVersion with the actual latest version from data
312+
useEffect(() => {
313+
if (queryVersion === 'latest' && fallbackVersion && selectedVersion !== fallbackVersion) {
314+
setSelectedVersion(fallbackVersion);
315+
}
316+
}, [queryVersion, fallbackVersion, selectedVersion]);
317+
310318
const schema = subjectData.schemas.first((x) => x.version === selectedVersion);
311319

312320
useEffect(() => {
@@ -645,6 +653,46 @@ const VersionDiff = (p: { subject: SchemaRegistrySubjectDetails }) => {
645653
);
646654
};
647655

656+
const SchemaMetadataSection = ({ schema }: { schema: SchemaRegistryVersionedSchema }) => {
657+
const metadata = schema.metadata;
658+
const properties = metadata?.properties;
659+
const hasProperties = properties && Object.keys(properties).length > 0;
660+
661+
return (
662+
<>
663+
<Text data-testid="schema-metadata-heading" fontSize="lg" fontWeight="bold" mt="20">
664+
Metadata
665+
</Text>
666+
<Text mb="4">Metadata associated with this schema version.</Text>
667+
668+
<Text fontSize="md" fontWeight="bold" mb="3">
669+
Properties
670+
</Text>
671+
672+
{hasProperties ? (
673+
<Table testId="schema-metadata-properties">
674+
<TableHeader>
675+
<TableRow>
676+
<TableHead width="sm">Key</TableHead>
677+
<TableHead>Value</TableHead>
678+
</TableRow>
679+
</TableHeader>
680+
<TableBody>
681+
{Object.entries(properties).map(([key, value]) => (
682+
<TableRow key={key}>
683+
<TableCell weight="semibold">{key}</TableCell>
684+
<TableCell>{value}</TableCell>
685+
</TableRow>
686+
))}
687+
</TableBody>
688+
</Table>
689+
) : (
690+
<Text color="gray.500">No properties defined.</Text>
691+
)}
692+
</>
693+
);
694+
};
695+
648696
const SchemaReferences = (p: { subject: SchemaRegistrySubjectDetails; schema: SchemaRegistryVersionedSchema }) => {
649697
const { subject, schema } = p;
650698
const version = schema.version;
@@ -654,6 +702,8 @@ const SchemaReferences = (p: { subject: SchemaRegistrySubjectDetails; schema: Sc
654702

655703
return (
656704
<>
705+
<SchemaMetadataSection schema={schema} />
706+
657707
<Text data-testid="schema-references-heading" fontSize="lg" fontWeight="bold" mt="20">
658708
References
659709
</Text>

0 commit comments

Comments
 (0)