Skip to content

Commit 2dcfc8d

Browse files
committed
feat: Implement catalog_incident api
1 parent d518a2f commit 2dcfc8d

File tree

10 files changed

+219
-207
lines changed

10 files changed

+219
-207
lines changed

catalog/api/app.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,49 @@ async def catalog_item_metrics(request):
587587
url=f"{reporting_api}/catalog_item/metrics/{asset_uuid}?use_cache=true",
588588
)
589589

590+
@routes.get("/api/catalog_incident/active-incidents")
591+
async def catalog_item_active_incidents(request):
592+
stage = request.query.get("stage")
593+
queryString = ""
594+
if stage:
595+
queryString = "?stage={stage}"
596+
headers = {
597+
"Authorization": f"Bearer {reporting_api_authorization_token}"
598+
}
599+
return await api_proxy(
600+
headers=headers,
601+
method="GET",
602+
url=f"{reporting_api}/catalog_incident/active-incidents{queryString}",
603+
)
604+
605+
@routes.get("/api/catalog_incident/last-incident/{asset_uuid}/{stage}")
606+
async def catalog_item_last_incident(request):
607+
asset_uuid = request.match_info.get('asset_uuid')
608+
stage = request.match_info.get('stage')
609+
headers = {
610+
"Authorization": f"Bearer {reporting_api_authorization_token}"
611+
}
612+
return await api_proxy(
613+
headers=headers,
614+
method="GET",
615+
url=f"{reporting_api}/catalog_incident/last-incidents/{asset_uuid}/{stage}",
616+
)
617+
618+
@routes.post("/api/catalog_incident/incidents/{asset_uuid}/{stage}")
619+
async def catalog_item_incidents(request):
620+
asset_uuid = request.match_info.get('asset_uuid')
621+
stage = request.match_info.get('stage')
622+
data = await request.json()
623+
headers = {
624+
"Authorization": f"Bearer {reporting_api_authorization_token}"
625+
}
626+
return await api_proxy(
627+
headers=headers,
628+
method="POST",
629+
data=json.dumps(data),
630+
url=f"{reporting_api}/catalog_incident/incidents/{asset_uuid}/{stage}",
631+
)
632+
590633
@routes.get("/api/workshop/{workshop_id}")
591634
async def workshop_get(request):
592635
"""

catalog/ui/src/app/Admin/CatalogItemAdmin.tsx

Lines changed: 51 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import {
2323
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
2424
import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
2525
import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon';
26-
import { apiPaths, fetcher, patchK8sObjectByPath } from '@app/api';
27-
import { CatalogItem } from '@app/types';
28-
import { BABYLON_DOMAIN, displayName } from '@app/util';
26+
import { apiPaths, fetcher } from '@app/api';
27+
import { CatalogItem, CatalogItemIncident, CatalogItemIncidentStatus } from '@app/types';
28+
import { displayName } from '@app/util';
2929
import CatalogItemIcon from '@app/Catalog/CatalogItemIcon';
30-
import { CUSTOM_LABELS, formatString, getProvider } from '@app/Catalog/catalog-utils';
30+
import { formatString, getProvider } from '@app/Catalog/catalog-utils';
3131
import OperationalLogo from '@app/components/StatusPageIcons/Operational';
3232
import DegradedPerformanceLogo from '@app/components/StatusPageIcons/DegradedPerformance';
3333
import PartialOutageLogo from '@app/components/StatusPageIcons/PartialOutage';
@@ -36,7 +36,6 @@ import UnderMaintenanceLogo from '@app/components/StatusPageIcons/UnderMaintenan
3636
import useSession from '@app/utils/useSession';
3737
import LocalTimestamp from '@app/components/LocalTimestamp';
3838
import LoadingIcon from '@app/components/LoadingIcon';
39-
import useMatchMutate from '@app/utils/useMatchMutate';
4039

4140
import './catalog-item-admin.css';
4241

@@ -45,53 +44,34 @@ type comment = {
4544
createdAt: string;
4645
message: string;
4746
};
48-
export type Ops = {
49-
disabled: boolean;
50-
status: {
51-
id: string;
52-
updated: {
53-
author: string;
54-
updatedAt: string;
55-
};
56-
};
57-
incidentUrl?: string;
58-
jiraIssueId?: string;
59-
comments: comment[];
60-
updated: {
61-
author: string;
62-
updatedAt: string;
63-
};
64-
};
6547

6648
const CatalogItemAdmin: React.FC = () => {
6749
const { namespace, name } = useParams();
6850
const navigate = useNavigate();
69-
const { data: catalogItem, mutate } = useSWR<CatalogItem>(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher);
70-
const matchMutate = useMatchMutate();
51+
const { data: catalogItem } = useSWR<CatalogItem>(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher);
52+
const asset_uuid = catalogItem.metadata.labels['gpte.redhat.com/asset-uuid'];
53+
const { data: catalogItemIncident } = useSWR<CatalogItemIncident>(
54+
apiPaths.CATALOG_ITEM_LAST_INCIDENT({ namespace, asset_uuid }),
55+
fetcher
56+
);
7157
const { email: userEmail } = useSession().getSession();
7258
const [isReadOnlyValue, setIsReadOnlyValue] = useState(false);
7359
const [isOpen, setIsOpen] = useState(false);
7460
const [isLoading, setIsLoading] = useState(false);
75-
const ops: Ops = catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`]
76-
? JSON.parse(catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`])
77-
: null;
78-
const disabled = catalogItem.metadata.labels?.[`${CUSTOM_LABELS.DISABLED.domain}/${CUSTOM_LABELS.DISABLED.key}`]
79-
? JSON.parse(catalogItem.metadata.labels?.[`${CUSTOM_LABELS.DISABLED.domain}/${CUSTOM_LABELS.DISABLED.key}`])
80-
: false;
81-
const [status, setStatus] = useState(ops?.status.id || 'operational');
82-
const [isDisabled, setIsDisabled] = useState(disabled);
83-
const [incidentUrl, setIncidentUrl] = useState(ops?.incidentUrl || '');
84-
const [jiraIssueId, setJiraIssueId] = useState(ops?.jiraIssueId || '');
61+
const [status, setStatus] = useState(catalogItemIncident.status || 'Operational');
62+
const [isDisabled, setIsDisabled] = useState(catalogItemIncident.disabled ?? false);
63+
const [incidentUrl, setIncidentUrl] = useState(catalogItemIncident.incident_url || '');
64+
const [jiraIssueId, setJiraIssueId] = useState(catalogItemIncident.jira_url || '');
8565
const [comment, setComment] = useState('');
8666
const provider = getProvider(catalogItem);
8767

8868
useEffect(() => {
89-
if (status === 'operational') {
69+
if (status === 'Operational') {
9070
setIsDisabled(false);
9171
setIsReadOnlyValue(true);
9272
setJiraIssueId('');
9373
setIncidentUrl('');
94-
} else if (status === 'major-outage') {
74+
} else if (status === 'Major outage') {
9575
setIsDisabled(true);
9676
setIsReadOnlyValue(true);
9777
} else {
@@ -100,70 +80,43 @@ const CatalogItemAdmin: React.FC = () => {
10080
}, [setIsReadOnlyValue, status]);
10181

10282
async function removeComment(comment: comment) {
103-
if (!ops?.comments || ops.comments.length < 1) {
83+
if (!catalogItemIncident?.comments) {
10484
throw "Can't find comment to delete";
10585
}
106-
const comments = ops.comments.filter((c) => c.createdAt !== comment.createdAt);
107-
const patch = {
108-
metadata: {
109-
annotations: { [`${BABYLON_DOMAIN}/ops`]: JSON.stringify({ ...ops, comments }) },
110-
},
111-
};
112-
setIsLoading(true);
113-
const catalogItemUpdated: CatalogItem = await patchK8sObjectByPath({
114-
path: apiPaths.CATALOG_ITEM({
115-
namespace,
116-
name,
117-
}),
118-
patch,
119-
});
120-
mutate(catalogItemUpdated);
121-
matchMutate([
122-
{ name: 'CATALOG_ITEMS', arguments: { namespace: 'all-catalogs' }, data: undefined },
123-
{ name: 'CATALOG_ITEMS', arguments: { namespace: catalogItemUpdated.metadata.namespace }, data: undefined },
124-
]);
125-
setIsLoading(false);
86+
const comments = JSON.parse(catalogItemIncident.comments);
87+
if (comments.length < 1) {
88+
throw "Can't find comment to delete";
89+
}
90+
const new_comments = comments.filter((c: comment) => c.createdAt !== comment.createdAt);
91+
await saveForm(new_comments);
12692
}
127-
async function saveForm() {
128-
const comments = ops?.comments || [];
93+
async function saveForm(comments?: comment[]) {
94+
setIsLoading(true);
95+
if (comments === null || comments === undefined) {
96+
comments = JSON.parse(catalogItemIncident?.comments) || [];
97+
}
12998
if (comment) {
13099
comments.push({
131100
message: comment,
132101
author: userEmail,
133102
createdAt: new Date().toISOString(),
134103
});
135104
}
136-
const patchObj = {
137-
status: {
138-
id: status,
139-
updated:
140-
ops?.status.id !== status ? { author: userEmail, updatedAt: new Date().toISOString() } : ops?.status.updated,
141-
},
142-
jiraIssueId,
143-
incidentUrl,
144-
updated: { author: userEmail, updatedAt: new Date().toISOString() },
145-
comments,
146-
};
147105

148-
const patch = {
149-
metadata: {
150-
annotations: { [`${BABYLON_DOMAIN}/ops`]: JSON.stringify(patchObj) },
151-
labels: { [`${BABYLON_DOMAIN}/${CUSTOM_LABELS.DISABLED.key}`]: isDisabled.toString() },
152-
},
153-
};
154-
setIsLoading(true);
155-
const catalogItemUpdated: CatalogItem = await patchK8sObjectByPath({
156-
path: apiPaths.CATALOG_ITEM({
157-
namespace,
158-
name,
106+
await fetch(apiPaths.CATALOG_ITEM_INCIDENTS({ asset_uuid, namespace }), {
107+
method: 'POST',
108+
body: JSON.stringify({
109+
created_by: userEmail,
110+
disabled: isDisabled,
111+
status,
112+
incident_url: incidentUrl,
113+
jira_url: jiraIssueId,
114+
comments,
159115
}),
160-
patch,
116+
headers: {
117+
'Content-Type': 'application/json',
118+
},
161119
});
162-
mutate(catalogItemUpdated);
163-
matchMutate([
164-
{ name: 'CATALOG_ITEMS', arguments: { namespace: 'all-catalogs' }, data: undefined },
165-
{ name: 'CATALOG_ITEMS', arguments: { namespace: catalogItemUpdated.metadata.namespace }, data: undefined },
166-
]);
167120
setIsLoading(false);
168121
navigate('/catalog');
169122
}
@@ -196,7 +149,7 @@ const CatalogItemAdmin: React.FC = () => {
196149
<Select
197150
aria-label="StatusPage.io status"
198151
onSelect={(_, value) => {
199-
setStatus(value.toString());
152+
setStatus(value.toString() as CatalogItemIncidentStatus);
200153
setIsOpen(false);
201154
}}
202155
onToggle={() => setIsOpen(!isOpen)}
@@ -205,19 +158,19 @@ const CatalogItemAdmin: React.FC = () => {
205158
variant={SelectVariant.single}
206159
className="select-wrapper"
207160
>
208-
<SelectOption key="operational" value="operational">
161+
<SelectOption key="operational" value="Operational">
209162
<OperationalLogo /> Operational
210163
</SelectOption>
211-
<SelectOption key="degraded-performance" value="degraded-performance">
164+
<SelectOption key="degraded-performance" value="Degraded performance">
212165
<DegradedPerformanceLogo /> Degraded performance
213166
</SelectOption>
214-
<SelectOption key="partial-outage" value="partial-outage">
167+
<SelectOption key="partial-outage" value="Partial outage">
215168
<PartialOutageLogo /> Partial outage
216169
</SelectOption>
217-
<SelectOption key="major-outage" value="major-outage">
170+
<SelectOption key="major-outage" value="Major outage">
218171
<MajorOutageLogo /> Major outage
219172
</SelectOption>
220-
<SelectOption key="under-maintenance" value="under-maintenance">
173+
<SelectOption key="under-maintenance" value="Under maintenance">
221174
<UnderMaintenanceLogo /> Under maintenance
222175
</SelectOption>
223176
</Select>
@@ -228,10 +181,10 @@ const CatalogItemAdmin: React.FC = () => {
228181
/>
229182
</Tooltip>
230183
</div>
231-
{ops ? (
184+
{catalogItemIncident ? (
232185
<p className="catalog-item-admin__author">
233-
Changed by: <b>{ops.status.updated.author} </b>-{' '}
234-
<LocalTimestamp className="catalog-item-admin__timestamp" timestamp={ops.status.updated.updatedAt} />
186+
Changed by: <b>{catalogItemIncident.created_by} </b>-{' '}
187+
<LocalTimestamp className="catalog-item-admin__timestamp" timestamp={catalogItemIncident.created_at} />
235188
</p>
236189
) : null}
237190
</FormGroup>
@@ -268,7 +221,7 @@ const CatalogItemAdmin: React.FC = () => {
268221
</FormGroup>
269222
<FormGroup fieldId="comment" label="Comments (only visible to admins)">
270223
<ul className="catalog-item-admin__comments">
271-
{(ops?.comments || []).map((comment) => (
224+
{(JSON.parse(catalogItemIncident?.comments) || []).map((comment: comment) => (
272225
<li key={comment.createdAt} className="catalog-item-admin__comment">
273226
<p className="catalog-item-admin__author">
274227
<b>{comment.author} </b>-{' '}
@@ -292,7 +245,7 @@ const CatalogItemAdmin: React.FC = () => {
292245
</FormGroup>
293246
<ActionList>
294247
<ActionListItem>
295-
<Button isAriaDisabled={false} isDisabled={isLoading} onClick={saveForm}>
248+
<Button isAriaDisabled={false} isDisabled={isLoading} onClick={() => saveForm()}>
296249
Save
297250
</Button>
298251
</ActionListItem>

catalog/ui/src/app/Catalog/Catalog.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import ThIcon from '@patternfly/react-icons/dist/js/icons/th-icon';
3232
import useSWRImmutable from 'swr/immutable';
3333
import { AsyncParser } from 'json2csv';
3434
import { apiPaths, fetcherItemsInAllPages } from '@app/api';
35-
import { CatalogItem } from '@app/types';
35+
import { CatalogItem, CatalogItemIncident, CatalogItemIncidents } from '@app/types';
3636
import useSession from '@app/utils/useSession';
3737
import SearchInputString from '@app/components/SearchInputString';
3838
import {
@@ -53,7 +53,6 @@ import {
5353
HIDDEN_LABELS,
5454
CUSTOM_LABELS,
5555
setLastFilter,
56-
getIsDisabled,
5756
getStatus,
5857
} from './catalog-utils';
5958
import CatalogCategorySelector from './CatalogCategorySelector';
@@ -337,6 +336,9 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
337336
apiPaths.CATALOG_ITEMS({ namespace: catalogNamespaceName ? catalogNamespaceName : 'all-catalogs' }),
338337
() => fetchCatalog(catalogNamespaceName ? [catalogNamespaceName] : catalogNamespaceNames)
339338
);
339+
const { data: activeIncidents } = useSWRImmutable<CatalogItemIncidents>(
340+
apiPaths.CATALOG_ITEMS_ACTIVE_INCIDENT({ namespace: catalogNamespaceName ? catalogNamespaceName : null })
341+
);
340342

341343
const catalogItems = useMemo(
342344
() => catalogItemsArr.filter((ci) => filterCatalogItemByAccessControl(ci, groups, isAdmin)),
@@ -350,6 +352,12 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
350352
if (c.spec.description) {
351353
catalogItemsCpy[i].spec.description.safe = stripTags(c.spec.description.content);
352354
}
355+
const incident = activeIncidents.items.find(
356+
(i) => i.asset_uuid === c.metadata.labels?.['gpte.redhat.com/asset-uuid']
357+
);
358+
if (incident) {
359+
catalogItemsCpy[i].metadata.annotations[`${BABYLON_DOMAIN}/incident`] = JSON.stringify(incident);
360+
}
353361
});
354362
const options = {
355363
minMatchCharLength: 3,
@@ -412,12 +420,15 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
412420
const operationalItems = [];
413421
const disabledItems = [];
414422
for (let catalogItem of items) {
415-
const isDisabled = getIsDisabled(catalogItem);
416-
const { code: status } = getStatus(catalogItem);
417-
if (status === 'under-maintenance' || isDisabled) {
418-
disabledItems.push(catalogItem);
419-
} else {
420-
operationalItems.push(catalogItem);
423+
const status = getStatus(catalogItem);
424+
if (status) {
425+
const isDisabled = status.disabled;
426+
const statusName = status.name;
427+
if (statusName === 'Under maintenance' || isDisabled) {
428+
disabledItems.push(catalogItem);
429+
} else {
430+
operationalItems.push(catalogItem);
431+
}
421432
}
422433
}
423434
return operationalItems.concat(disabledItems);

0 commit comments

Comments
 (0)