Skip to content

Commit

Permalink
feat: Implement catalog_incident api
Browse files Browse the repository at this point in the history
  • Loading branch information
aleixhub committed Sep 18, 2024
1 parent d518a2f commit 2dcfc8d
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 207 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
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 fetch(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) => (
{(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
27 changes: 19 additions & 8 deletions catalog/ui/src/app/Catalog/Catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import ThIcon from '@patternfly/react-icons/dist/js/icons/th-icon';
import useSWRImmutable from 'swr/immutable';
import { AsyncParser } from 'json2csv';
import { apiPaths, fetcherItemsInAllPages } from '@app/api';
import { CatalogItem } from '@app/types';
import { CatalogItem, CatalogItemIncident, CatalogItemIncidents } from '@app/types';
import useSession from '@app/utils/useSession';
import SearchInputString from '@app/components/SearchInputString';
import {
Expand All @@ -53,7 +53,6 @@ import {
HIDDEN_LABELS,
CUSTOM_LABELS,
setLastFilter,
getIsDisabled,
getStatus,
} from './catalog-utils';
import CatalogCategorySelector from './CatalogCategorySelector';
Expand Down Expand Up @@ -337,6 +336,9 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
apiPaths.CATALOG_ITEMS({ namespace: catalogNamespaceName ? catalogNamespaceName : 'all-catalogs' }),
() => fetchCatalog(catalogNamespaceName ? [catalogNamespaceName] : catalogNamespaceNames)
);
const { data: activeIncidents } = useSWRImmutable<CatalogItemIncidents>(
apiPaths.CATALOG_ITEMS_ACTIVE_INCIDENT({ namespace: catalogNamespaceName ? catalogNamespaceName : null })
);

const catalogItems = useMemo(
() => catalogItemsArr.filter((ci) => filterCatalogItemByAccessControl(ci, groups, isAdmin)),
Expand All @@ -350,6 +352,12 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
if (c.spec.description) {
catalogItemsCpy[i].spec.description.safe = stripTags(c.spec.description.content);
}
const incident = activeIncidents.items.find(
(i) => i.asset_uuid === c.metadata.labels?.['gpte.redhat.com/asset-uuid']
);
if (incident) {
catalogItemsCpy[i].metadata.annotations[`${BABYLON_DOMAIN}/incident`] = JSON.stringify(incident);
}
});
const options = {
minMatchCharLength: 3,
Expand Down Expand Up @@ -412,12 +420,15 @@ const Catalog: React.FC<{ userHasRequiredPropertiesToAccess: boolean }> = ({ use
const operationalItems = [];
const disabledItems = [];
for (let catalogItem of items) {
const isDisabled = getIsDisabled(catalogItem);
const { code: status } = getStatus(catalogItem);
if (status === 'under-maintenance' || isDisabled) {
disabledItems.push(catalogItem);
} else {
operationalItems.push(catalogItem);
const status = getStatus(catalogItem);
if (status) {
const isDisabled = status.disabled;
const statusName = status.name;
if (statusName === 'Under maintenance' || isDisabled) {
disabledItems.push(catalogItem);
} else {
operationalItems.push(catalogItem);
}
}
}
return operationalItems.concat(disabledItems);
Expand Down
Loading

0 comments on commit 2dcfc8d

Please sign in to comment.