Skip to content

Commit

Permalink
feat: Implement catalog_incident api (#2146)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleixhub authored Sep 18, 2024
1 parent d518a2f commit c62a376
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 356 deletions.
43 changes: 43 additions & 0 deletions catalog/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,49 @@ async def catalog_item_metrics(request):
url=f"{reporting_api}/catalog_item/metrics/{asset_uuid}?use_cache=true",
)

@routes.get("/api/catalog_incident/active-incidents")
async def catalog_item_active_incidents(request):
stage = request.query.get("stage")
queryString = ""
if stage:
queryString = "?stage={stage}"
headers = {
"Authorization": f"Bearer {reporting_api_authorization_token}"
}
return await api_proxy(
headers=headers,
method="GET",
url=f"{reporting_api}/catalog_incident/active-incidents{queryString}",
)

@routes.get("/api/catalog_incident/last-incident/{asset_uuid}/{stage}")
async def catalog_item_last_incident(request):
asset_uuid = request.match_info.get('asset_uuid')
stage = request.match_info.get('stage')
headers = {
"Authorization": f"Bearer {reporting_api_authorization_token}"
}
return await api_proxy(
headers=headers,
method="GET",
url=f"{reporting_api}/catalog_incident/last-incidents/{asset_uuid}/{stage}",
)

@routes.post("/api/catalog_incident/incidents/{asset_uuid}/{stage}")
async def catalog_item_incidents(request):
asset_uuid = request.match_info.get('asset_uuid')
stage = request.match_info.get('stage')
data = await request.json()
headers = {
"Authorization": f"Bearer {reporting_api_authorization_token}"
}
return await api_proxy(
headers=headers,
method="POST",
data=json.dumps(data),
url=f"{reporting_api}/catalog_incident/incidents/{asset_uuid}/{stage}",
)

@routes.get("/api/workshop/{workshop_id}")
async def workshop_get(request):
"""
Expand Down
2 changes: 1 addition & 1 deletion catalog/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ api:
threads: 1
image:
#override:
tag: v1.0.5
tag: v1.0.6
repository: quay.io/redhat-gpte/babylon-catalog-api
pullPolicy: IfNotPresent
imagePullSecrets: []
Expand Down
45 changes: 21 additions & 24 deletions catalog/ui/src/app/Admin/CatalogItemAdmin.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ jest.mock('../api');
import React from 'react';
import { generateSession, render, waitFor } from '../utils/test-utils';
import CatalogItemAdmin from './CatalogItemAdmin';
import catalogItemObj from '../__mocks__/catalogItem--disabled.json';
import { CatalogItem } from '@app/types';
import catalogItemObj from '../__mocks__/catalogItem.json';
import catalogItemIncident from '../__mocks__/catalogItemIncident.json';
import { CatalogItem, CatalogItemIncident } from '@app/types';
import userEvent from '@testing-library/user-event';
import { apiPaths, patchK8sObjectByPath } from '@app/api';
import { BABYLON_DOMAIN } from '@app/util';
import { apiPaths, fetcher } from '@app/api';

const namespaceName = 'fakeNamespace';
const ciName = 'ci-name';
const asset_uuid = 'c8a5d5ab-1b17-4c6a-866a-fe60de5482b4'

jest.mock('@app/api', () => ({
...jest.requireActual('@app/api'),
fetcher: jest.fn(() => Promise.resolve(catalogItemObj as CatalogItem)),
patchK8sObjectByPath: jest.fn(() => Promise.resolve(catalogItemObj as CatalogItem)),
fetcher: jest.fn((...args) => {
if (args[0] === apiPaths.CATALOG_ITEM_LAST_INCIDENT({ namespace: namespaceName, asset_uuid })) {
return Promise.resolve(catalogItemIncident as CatalogItemIncident);
}
return Promise.resolve(catalogItemObj as CatalogItem);
}),
}));

jest.mock('react-router-dom', () => ({
Expand All @@ -41,9 +46,6 @@ describe('CatalogItemAdmin Component', () => {
});
});
test('When save form API function is called', async () => {
const mockDate = new Date();
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
Date.parse = jest.fn(() => 1656950267699);
const { getByLabelText, getByText } = render(<CatalogItemAdmin />);

await waitFor(() => {
Expand All @@ -52,24 +54,19 @@ describe('CatalogItemAdmin Component', () => {
await userEvent.click(getByLabelText('Disabled'));
await userEvent.click(getByText('Under maintenance').closest('button'));
await userEvent.click(getByText('Operational'));
const path = apiPaths.CATALOG_ITEM({
const path = apiPaths.CATALOG_ITEM_INCIDENTS({
namespace: namespaceName,
name: ciName,
asset_uuid,
});
const patchObj = {
status: { id: 'operational', updated: { author: '[email protected]', updatedAt: mockDate.toISOString() } },
jiraIssueId: '',
incidentUrl: '',
updated: { author: '[email protected]', updatedAt: mockDate.toISOString() },
comments: [],
};
const patch = {
metadata: {
annotations: { [`${BABYLON_DOMAIN}/ops`]: JSON.stringify(patchObj) },
labels: { [`${BABYLON_DOMAIN}/disabled`]: 'false' },
},
};
created_by: '[email protected]',
disabled: false,
status: 'Operational',
incident_url: '',
jira_url: '',
comments: [],
}
await userEvent.click(getByText('Save'));
expect(patchK8sObjectByPath).toHaveBeenCalledWith({ path, patch });
expect(fetcher).toHaveBeenCalledWith(path, { method: 'POST', body: JSON.stringify(patch), headers: {'Content-Type': 'application/json'}});
});
});
149 changes: 51 additions & 98 deletions catalog/ui/src/app/Admin/CatalogItemAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import {
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
import TrashIcon from '@patternfly/react-icons/dist/js/icons/trash-icon';
import { apiPaths, fetcher, patchK8sObjectByPath } from '@app/api';
import { CatalogItem } from '@app/types';
import { BABYLON_DOMAIN, displayName } from '@app/util';
import { apiPaths, fetcher } from '@app/api';
import { CatalogItem, CatalogItemIncident, CatalogItemIncidentStatus } from '@app/types';
import { displayName } from '@app/util';
import CatalogItemIcon from '@app/Catalog/CatalogItemIcon';
import { CUSTOM_LABELS, formatString, getProvider } from '@app/Catalog/catalog-utils';
import { formatString, getProvider } from '@app/Catalog/catalog-utils';
import OperationalLogo from '@app/components/StatusPageIcons/Operational';
import DegradedPerformanceLogo from '@app/components/StatusPageIcons/DegradedPerformance';
import PartialOutageLogo from '@app/components/StatusPageIcons/PartialOutage';
Expand All @@ -36,7 +36,6 @@ import UnderMaintenanceLogo from '@app/components/StatusPageIcons/UnderMaintenan
import useSession from '@app/utils/useSession';
import LocalTimestamp from '@app/components/LocalTimestamp';
import LoadingIcon from '@app/components/LoadingIcon';
import useMatchMutate from '@app/utils/useMatchMutate';

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

Expand All @@ -45,53 +44,34 @@ type comment = {
createdAt: string;
message: string;
};
export type Ops = {
disabled: boolean;
status: {
id: string;
updated: {
author: string;
updatedAt: string;
};
};
incidentUrl?: string;
jiraIssueId?: string;
comments: comment[];
updated: {
author: string;
updatedAt: string;
};
};

const CatalogItemAdmin: React.FC = () => {
const { namespace, name } = useParams();
const navigate = useNavigate();
const { data: catalogItem, mutate } = useSWR<CatalogItem>(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher);
const matchMutate = useMatchMutate();
const { data: catalogItem } = useSWR<CatalogItem>(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher);
const asset_uuid = catalogItem.metadata.labels['gpte.redhat.com/asset-uuid'];
const { data: catalogItemIncident } = useSWR<CatalogItemIncident>(
apiPaths.CATALOG_ITEM_LAST_INCIDENT({ namespace, asset_uuid }),
fetcher
);
const { email: userEmail } = useSession().getSession();
const [isReadOnlyValue, setIsReadOnlyValue] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const ops: Ops = catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`]
? JSON.parse(catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`])
: null;
const disabled = catalogItem.metadata.labels?.[`${CUSTOM_LABELS.DISABLED.domain}/${CUSTOM_LABELS.DISABLED.key}`]
? JSON.parse(catalogItem.metadata.labels?.[`${CUSTOM_LABELS.DISABLED.domain}/${CUSTOM_LABELS.DISABLED.key}`])
: false;
const [status, setStatus] = useState(ops?.status.id || 'operational');
const [isDisabled, setIsDisabled] = useState(disabled);
const [incidentUrl, setIncidentUrl] = useState(ops?.incidentUrl || '');
const [jiraIssueId, setJiraIssueId] = useState(ops?.jiraIssueId || '');
const [status, setStatus] = useState(catalogItemIncident?.status || 'Operational');
const [isDisabled, setIsDisabled] = useState(catalogItemIncident?.disabled ?? false);
const [incidentUrl, setIncidentUrl] = useState(catalogItemIncident?.incident_url || '');
const [jiraIssueId, setJiraIssueId] = useState(catalogItemIncident?.jira_url || '');
const [comment, setComment] = useState('');
const provider = getProvider(catalogItem);

useEffect(() => {
if (status === 'operational') {
if (status === 'Operational') {
setIsDisabled(false);
setIsReadOnlyValue(true);
setJiraIssueId('');
setIncidentUrl('');
} else if (status === 'major-outage') {
} else if (status === 'Major outage') {
setIsDisabled(true);
setIsReadOnlyValue(true);
} else {
Expand All @@ -100,70 +80,43 @@ const CatalogItemAdmin: React.FC = () => {
}, [setIsReadOnlyValue, status]);

async function removeComment(comment: comment) {
if (!ops?.comments || ops.comments.length < 1) {
if (!catalogItemIncident?.comments) {
throw "Can't find comment to delete";
}
const comments = ops.comments.filter((c) => c.createdAt !== comment.createdAt);
const patch = {
metadata: {
annotations: { [`${BABYLON_DOMAIN}/ops`]: JSON.stringify({ ...ops, comments }) },
},
};
setIsLoading(true);
const catalogItemUpdated: CatalogItem = await patchK8sObjectByPath({
path: apiPaths.CATALOG_ITEM({
namespace,
name,
}),
patch,
});
mutate(catalogItemUpdated);
matchMutate([
{ name: 'CATALOG_ITEMS', arguments: { namespace: 'all-catalogs' }, data: undefined },
{ name: 'CATALOG_ITEMS', arguments: { namespace: catalogItemUpdated.metadata.namespace }, data: undefined },
]);
setIsLoading(false);
const comments = JSON.parse(catalogItemIncident.comments);
if (comments.length < 1) {
throw "Can't find comment to delete";
}
const new_comments = comments.filter((c: comment) => c.createdAt !== comment.createdAt);
await saveForm(new_comments);
}
async function saveForm() {
const comments = ops?.comments || [];
async function saveForm(comments?: comment[]) {
setIsLoading(true);
if (comments === null || comments === undefined) {
comments = JSON.parse(catalogItemIncident?.comments) || [];
}
if (comment) {
comments.push({
message: comment,
author: userEmail,
createdAt: new Date().toISOString(),
});
}
const patchObj = {
status: {
id: status,
updated:
ops?.status.id !== status ? { author: userEmail, updatedAt: new Date().toISOString() } : ops?.status.updated,
},
jiraIssueId,
incidentUrl,
updated: { author: userEmail, updatedAt: new Date().toISOString() },
comments,
};

const patch = {
metadata: {
annotations: { [`${BABYLON_DOMAIN}/ops`]: JSON.stringify(patchObj) },
labels: { [`${BABYLON_DOMAIN}/${CUSTOM_LABELS.DISABLED.key}`]: isDisabled.toString() },
},
};
setIsLoading(true);
const catalogItemUpdated: CatalogItem = await patchK8sObjectByPath({
path: apiPaths.CATALOG_ITEM({
namespace,
name,
await fetcher(apiPaths.CATALOG_ITEM_INCIDENTS({ asset_uuid, namespace }), {
method: 'POST',
body: JSON.stringify({
created_by: userEmail,
disabled: isDisabled,
status,
incident_url: incidentUrl,
jira_url: jiraIssueId,
comments,
}),
patch,
headers: {
'Content-Type': 'application/json',
},
});
mutate(catalogItemUpdated);
matchMutate([
{ name: 'CATALOG_ITEMS', arguments: { namespace: 'all-catalogs' }, data: undefined },
{ name: 'CATALOG_ITEMS', arguments: { namespace: catalogItemUpdated.metadata.namespace }, data: undefined },
]);
setIsLoading(false);
navigate('/catalog');
}
Expand Down Expand Up @@ -196,7 +149,7 @@ const CatalogItemAdmin: React.FC = () => {
<Select
aria-label="StatusPage.io status"
onSelect={(_, value) => {
setStatus(value.toString());
setStatus(value.toString() as CatalogItemIncidentStatus);
setIsOpen(false);
}}
onToggle={() => setIsOpen(!isOpen)}
Expand All @@ -205,19 +158,19 @@ const CatalogItemAdmin: React.FC = () => {
variant={SelectVariant.single}
className="select-wrapper"
>
<SelectOption key="operational" value="operational">
<SelectOption key="operational" value="Operational">
<OperationalLogo /> Operational
</SelectOption>
<SelectOption key="degraded-performance" value="degraded-performance">
<SelectOption key="degraded-performance" value="Degraded performance">
<DegradedPerformanceLogo /> Degraded performance
</SelectOption>
<SelectOption key="partial-outage" value="partial-outage">
<SelectOption key="partial-outage" value="Partial outage">
<PartialOutageLogo /> Partial outage
</SelectOption>
<SelectOption key="major-outage" value="major-outage">
<SelectOption key="major-outage" value="Major outage">
<MajorOutageLogo /> Major outage
</SelectOption>
<SelectOption key="under-maintenance" value="under-maintenance">
<SelectOption key="under-maintenance" value="Under maintenance">
<UnderMaintenanceLogo /> Under maintenance
</SelectOption>
</Select>
Expand All @@ -228,10 +181,10 @@ const CatalogItemAdmin: React.FC = () => {
/>
</Tooltip>
</div>
{ops ? (
{catalogItemIncident ? (
<p className="catalog-item-admin__author">
Changed by: <b>{ops.status.updated.author} </b>-{' '}
<LocalTimestamp className="catalog-item-admin__timestamp" timestamp={ops.status.updated.updatedAt} />
Changed by: <b>{catalogItemIncident.created_by} </b>-{' '}
<LocalTimestamp className="catalog-item-admin__timestamp" timestamp={catalogItemIncident.created_at} />
</p>
) : null}
</FormGroup>
Expand Down Expand Up @@ -268,7 +221,7 @@ const CatalogItemAdmin: React.FC = () => {
</FormGroup>
<FormGroup fieldId="comment" label="Comments (only visible to admins)">
<ul className="catalog-item-admin__comments">
{(ops?.comments || []).map((comment) => (
{(catalogItemIncident ? JSON.parse(catalogItemIncident.comments) : []).map((comment: comment) => (
<li key={comment.createdAt} className="catalog-item-admin__comment">
<p className="catalog-item-admin__author">
<b>{comment.author} </b>-{' '}
Expand All @@ -292,7 +245,7 @@ const CatalogItemAdmin: React.FC = () => {
</FormGroup>
<ActionList>
<ActionListItem>
<Button isAriaDisabled={false} isDisabled={isLoading} onClick={saveForm}>
<Button isAriaDisabled={false} isDisabled={isLoading} onClick={() => saveForm()}>
Save
</Button>
</ActionListItem>
Expand Down
Loading

0 comments on commit c62a376

Please sign in to comment.