Skip to content

Commit 72c71b8

Browse files
committed
frontend: refactor a8r.io annotation handling into shared helper
1 parent 568107c commit 72c71b8

File tree

2 files changed

+94
-157
lines changed

2 files changed

+94
-157
lines changed

frontend/src/components/service/A8RServiceInfo.tsx

Lines changed: 71 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,49 @@
1717
import { Icon } from '@iconify/react';
1818
import Box from '@mui/material/Box';
1919
import Typography from '@mui/material/Typography';
20-
import _ from 'lodash';
20+
import React from 'react';
2121
import Link from '../common/Link';
2222

23-
const A8R_ICON_MAP: Record<string, string> = {
24-
description: 'mdi:information-outline',
25-
owner: 'mdi:account',
26-
chat: 'mdi:chat',
27-
bugs: 'mdi:bug',
28-
logs: 'mdi:chart-timeline-variant',
29-
documentation: 'mdi:book-open-page-variant',
30-
repository: 'mdi:github',
31-
support: 'mdi:help-circle',
32-
runbook: 'mdi:script-text',
33-
incidents: 'mdi:alert-circle',
34-
uptime: 'mdi:check-circle',
35-
performance: 'mdi:speedometer',
36-
dependencies: 'mdi:sitemap',
23+
export type A8RMetadataItem = {
24+
key: string;
25+
label: string;
26+
value: string;
27+
icon: string;
28+
isLink: boolean;
3729
};
3830

39-
const LINKABLE_KEYS = [
31+
const A8R_ICON_MAP: Record<string, { icon: string; label: string }> = {
32+
description: { icon: 'mdi:information-outline', label: 'Description' },
33+
owner: { icon: 'mdi:account', label: 'Owner' },
34+
dependencies: { icon: 'mdi:sitemap', label: 'Dependencies' },
35+
chat: { icon: 'mdi:chat', label: 'Chat' },
36+
bugs: { icon: 'mdi:bug', label: 'Bugs' },
37+
logs: { icon: 'mdi:chart-timeline-variant', label: 'Logs' },
38+
documentation: { icon: 'mdi:book-open-page-variant', label: 'Documentation' },
39+
repository: { icon: 'mdi:github', label: 'Repository' },
40+
support: { icon: 'mdi:help-circle', label: 'Support' },
41+
runbook: { icon: 'mdi:script-text', label: 'Runbook' },
42+
incidents: { icon: 'mdi:alert-circle', label: 'Incidents' },
43+
uptime: { icon: 'mdi:check-circle', label: 'Uptime' },
44+
performance: { icon: 'mdi:speedometer', label: 'Performance' },
45+
};
46+
47+
const PREFERRED_ORDER = [
48+
'description',
49+
'owner',
50+
'dependencies',
4051
'chat',
41-
'bugs',
42-
'logs',
4352
'documentation',
4453
'repository',
54+
'logs',
55+
'bugs',
4556
'support',
4657
'runbook',
4758
'incidents',
4859
'uptime',
4960
'performance',
5061
];
62+
5163
function isValidHttpUrl(value: string) {
5264
try {
5365
const url = new URL(value);
@@ -56,68 +68,52 @@ function isValidHttpUrl(value: string) {
5668
return false;
5769
}
5870
}
71+
export function getA8RMetadata(annotations: Record<string, string> = {}): A8RMetadataItem[] {
72+
return Object.entries(annotations)
73+
.filter(([key]) => key.startsWith('a8r.io/'))
74+
.map(([key, value]) => {
75+
const shortKey = key.replace('a8r.io/', '');
76+
const meta = A8R_ICON_MAP[shortKey];
77+
if (!meta) return null;
78+
79+
return {
80+
key: shortKey,
81+
label: meta.label,
82+
value,
83+
icon: meta.icon,
84+
isLink: isValidHttpUrl(value),
85+
};
86+
})
87+
.filter((v): v is A8RMetadataItem => Boolean(v))
88+
.sort((a, b) => {
89+
const ai = PREFERRED_ORDER.indexOf(a.key);
90+
const bi = PREFERRED_ORDER.indexOf(b.key);
91+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
92+
});
93+
}
5994

6095
export default function A8RServiceInfo({ annotations }: { annotations?: Record<string, string> }) {
61-
//Defining the order of annotations for user friendly display
62-
const PREFERRED_ORDER = [
63-
'description',
64-
'owner',
65-
'dependencies',
66-
'chat',
67-
'documentation',
68-
'repository',
69-
'logs',
70-
'bugs',
71-
'support',
72-
'runbook',
73-
'incidents',
74-
'uptime',
75-
'performance',
76-
];
77-
const a8rEntries = Object.entries(annotations || {})
78-
.filter(([key]) => key.startsWith('a8r.io/'))
79-
.map(([key, value]) => ({
80-
type: key.replace('a8r.io/', ''),
81-
value,
82-
originalKey: key,
83-
}));
96+
const metadata = React.useMemo(() => getA8RMetadata(annotations), [annotations]);
8497

85-
const sortedEntries = _.sortBy(a8rEntries, entry => {
86-
const index = PREFERRED_ORDER.indexOf(entry.type);
87-
return index === -1 ? 999 : index;
88-
});
98+
if (metadata.length === 0) return null;
8999

90-
if (sortedEntries.length === 0) return null;
91100
return (
92-
<Box display="flex" flexDirection="column" gap={1.5} py={1}>
93-
{sortedEntries.map(({ type, value, originalKey }) => {
94-
const icon = A8R_ICON_MAP[type] || 'mdi:tag-outline';
95-
const isLink = LINKABLE_KEYS.includes(type) && isValidHttpUrl(value);
96-
const label = _.capitalize(type);
97-
const renderedValue =
98-
type === 'dependencies'
99-
? value
100-
.split(',')
101-
.map(s => s.trim())
102-
.join(', ')
103-
: value;
104-
105-
return (
106-
<Box key={originalKey} display="flex" alignItems="center">
107-
<Icon icon={icon} width="20" style={{ marginRight: 8, color: 'text.secondary' }} />
108-
<Typography variant="body2">
109-
<strong>{label}:</strong>{' '}
110-
{isLink ? (
111-
<Link href={value} target="_blank" rel="noopener">
112-
{value}
113-
</Link>
114-
) : (
115-
renderedValue
116-
)}
117-
</Typography>
118-
</Box>
119-
);
120-
})}
101+
<Box display="flex" flexDirection="column" gap={1.5}>
102+
{metadata.map(item => (
103+
<Box key={item.key} display="flex" alignItems="center">
104+
<Icon icon={item.icon} width="20" style={{ marginRight: 8 }} />
105+
<Typography variant="body2">
106+
<strong>{item.label}:</strong>{' '}
107+
{item.isLink ? (
108+
<Link href={item.value} target="_blank" rel="noopener noreferrer">
109+
{item.value}
110+
</Link>
111+
) : (
112+
item.value
113+
)}
114+
</Typography>
115+
</Box>
116+
))}
121117
</Box>
122118
);
123119
}

frontend/src/components/service/List.tsx

Lines changed: 23 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,8 @@ import Service from '../../lib/k8s/service';
2323
import LabelListItem from '../common/LabelListItem';
2424
import { MetadataDictGrid } from '../common/Resource';
2525
import ResourceListView from '../common/Resource/ResourceListView';
26+
import { getA8RMetadata } from './A8RServiceInfo';
2627

27-
function isValidHttpUrl(value: string) {
28-
try {
29-
const url = new URL(value);
30-
return url.protocol === 'http:' || url.protocol === 'https:';
31-
} catch {
32-
return false;
33-
}
34-
}
3528
export default function ServiceList() {
3629
const { t } = useTranslation(['glossary', 'translation']);
3730

@@ -88,82 +81,30 @@ export default function ServiceList() {
8881
]}
8982
actions={[
9083
{
91-
id: 'a8r-logs',
84+
id: 'a8r-actions',
9285
action: ({ item, closeMenu }) => {
93-
const url = item.metadata.annotations?.['a8r.io/logs'];
94-
if (!url || !isValidHttpUrl(url)) return null;
95-
return (
96-
<MenuItem
97-
onClick={() => {
98-
window.open(url, '_blank', 'noopener,noreferrer');
99-
closeMenu();
100-
}}
101-
>
102-
<ListItemIcon>
103-
<Icon icon="mdi:chart-timeline-variant" width="20" />
104-
</ListItemIcon>
105-
<ListItemText>{t('View Logs')}</ListItemText>
106-
</MenuItem>
107-
);
108-
},
109-
},
110-
{
111-
id: 'a8r-docs',
112-
action: ({ item, closeMenu }) => {
113-
const url = item.metadata.annotations?.['a8r.io/documentation'];
114-
if (!url || !isValidHttpUrl(url)) return null;
115-
return (
116-
<MenuItem
117-
onClick={() => {
118-
window.open(url, '_blank', 'noopener,noreferrer');
119-
closeMenu();
120-
}}
121-
>
122-
<ListItemIcon>
123-
<Icon icon="mdi:book-open-page-variant" width="20" />
124-
</ListItemIcon>
125-
<ListItemText>{t('View Documentation')}</ListItemText>
126-
</MenuItem>
127-
);
128-
},
129-
},
130-
{
131-
id: 'a8r-repo',
132-
action: ({ item, closeMenu }) => {
133-
const url = item.metadata.annotations?.['a8r.io/repository'];
134-
if (!url || !isValidHttpUrl(url)) return null;
135-
return (
136-
<MenuItem
137-
onClick={() => {
138-
window.open(url, '_blank', 'noopener,noreferrer');
139-
closeMenu();
140-
}}
141-
>
142-
<ListItemIcon>
143-
<Icon icon="mdi:github" width="20" />
144-
</ListItemIcon>
145-
<ListItemText>{t('View Repository')}</ListItemText>
146-
</MenuItem>
147-
);
148-
},
149-
},
150-
{
151-
id: 'a8r-chat',
152-
action: ({ item, closeMenu }) => {
153-
const url = item.metadata.annotations?.['a8r.io/chat'];
154-
if (!url || !isValidHttpUrl(url)) return null;
86+
const annotations = item.metadata.annotations ?? {};
87+
const metadata = getA8RMetadata(annotations).filter(m => m.isLink);
88+
89+
if (metadata.length === 0) return null;
90+
15591
return (
156-
<MenuItem
157-
onClick={() => {
158-
window.open(url, '_blank', 'noopener,noreferrer');
159-
closeMenu();
160-
}}
161-
>
162-
<ListItemIcon>
163-
<Icon icon="mdi:chat" width="20" />
164-
</ListItemIcon>
165-
<ListItemText>{t('Contact Team')}</ListItemText>
166-
</MenuItem>
92+
<>
93+
{metadata.map(meta => (
94+
<MenuItem
95+
key={meta.key}
96+
onClick={() => {
97+
window.open(meta.value, '_blank', 'noopener,noreferrer');
98+
closeMenu();
99+
}}
100+
>
101+
<ListItemIcon>
102+
<Icon icon={meta.icon} width="20" />
103+
</ListItemIcon>
104+
<ListItemText>{meta.label}</ListItemText>
105+
</MenuItem>
106+
))}
107+
</>
167108
);
168109
},
169110
},

0 commit comments

Comments
 (0)