diff --git a/catalog/api/app.py b/catalog/api/app.py index 85c23e9c4..cad809ad4 100644 --- a/catalog/api/app.py +++ b/catalog/api/app.py @@ -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): """ diff --git a/catalog/ui/src/app/Admin/CatalogItemAdmin.tsx b/catalog/ui/src/app/Admin/CatalogItemAdmin.tsx index 600adc5f5..6fd8c2939 100644 --- a/catalog/ui/src/app/Admin/CatalogItemAdmin.tsx +++ b/catalog/ui/src/app/Admin/CatalogItemAdmin.tsx @@ -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'; @@ -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'; @@ -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(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher); - const matchMutate = useMatchMutate(); + const { data: catalogItem } = useSWR(apiPaths.CATALOG_ITEM({ namespace, name }), fetcher); + const asset_uuid = catalogItem.metadata.labels['gpte.redhat.com/asset-uuid']; + const { data: catalogItemIncident } = useSWR( + 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 { @@ -100,32 +80,21 @@ 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, @@ -133,37 +102,21 @@ const CatalogItemAdmin: React.FC = () => { 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'); } @@ -196,7 +149,7 @@ const CatalogItemAdmin: React.FC = () => { @@ -228,10 +181,10 @@ const CatalogItemAdmin: React.FC = () => { /> - {ops ? ( + {catalogItemIncident ? (

- Changed by: {ops.status.updated.author} -{' '} - + Changed by: {catalogItemIncident.created_by} -{' '} +

) : null} @@ -268,7 +221,7 @@ const CatalogItemAdmin: React.FC = () => {
    - {(ops?.comments || []).map((comment) => ( + {(JSON.parse(catalogItemIncident?.comments) || []).map((comment: comment) => (
  • {comment.author} -{' '} @@ -292,7 +245,7 @@ const CatalogItemAdmin: React.FC = () => { - diff --git a/catalog/ui/src/app/Catalog/Catalog.tsx b/catalog/ui/src/app/Catalog/Catalog.tsx index 9502114dd..dc1d2648c 100644 --- a/catalog/ui/src/app/Catalog/Catalog.tsx +++ b/catalog/ui/src/app/Catalog/Catalog.tsx @@ -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 { @@ -53,7 +53,6 @@ import { HIDDEN_LABELS, CUSTOM_LABELS, setLastFilter, - getIsDisabled, getStatus, } from './catalog-utils'; import CatalogCategorySelector from './CatalogCategorySelector'; @@ -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( + apiPaths.CATALOG_ITEMS_ACTIVE_INCIDENT({ namespace: catalogNamespaceName ? catalogNamespaceName : null }) + ); const catalogItems = useMemo( () => catalogItemsArr.filter((ci) => filterCatalogItemByAccessControl(ci, groups, isAdmin)), @@ -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, @@ -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); diff --git a/catalog/ui/src/app/Catalog/CatalogItemCard.tsx b/catalog/ui/src/app/Catalog/CatalogItemCard.tsx index 2c9578c82..21d7e264a 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemCard.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemCard.tsx @@ -5,16 +5,7 @@ import { CatalogItem } from '@app/types'; import StatusPageIcons from '@app/components/StatusPageIcons'; import { displayName, renderContent, stripHtml } from '@app/util'; import StarRating from '@app/components/StarRating'; -import { - formatString, - getDescription, - getIsDisabled, - getProvider, - getRating, - getStage, - getStatus, - getSLA, -} from './catalog-utils'; +import { formatString, getDescription, getProvider, getRating, getStage, getStatus, getSLA } from './catalog-utils'; import CatalogItemIcon from './CatalogItemIcon'; import './catalog-item-card.css'; @@ -26,9 +17,8 @@ const CatalogItemCard: React.FC<{ catalogItem: CatalogItem }> = ({ catalogItem } const { description, descriptionFormat } = getDescription(catalogItem); const provider = getProvider(catalogItem); const stage = getStage(catalogItem); - const isDisabled = getIsDisabled(catalogItem); const rating = getRating(catalogItem); - const { code: status } = getStatus(catalogItem); + const status = getStatus(catalogItem); const sla = getSLA(catalogItem); if (namespace) { @@ -55,15 +45,15 @@ const CatalogItemCard: React.FC<{ catalogItem: CatalogItem }> = ({ catalogItem } ) : null} - {status && status !== 'operational' ? ( - + {status && status.name !== 'Operational' ? ( + ) : null} diff --git a/catalog/ui/src/app/Catalog/CatalogItemDetails.tsx b/catalog/ui/src/app/Catalog/CatalogItemDetails.tsx index 65661cc51..8942f2c3a 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemDetails.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemDetails.tsx @@ -26,7 +26,7 @@ import { import InfoAltIcon from '@patternfly/react-icons/dist/js/icons/info-alt-icon'; import useSWR from 'swr'; import { apiPaths, fetcher, fetcherItemsInAllPages } from '@app/api'; -import { AssetMetrics, CatalogItem, ResourceClaim } from '@app/types'; +import { AssetMetrics, CatalogItem, CatalogItemIncident, ResourceClaim } from '@app/types'; import LoadingIcon from '@app/components/LoadingIcon'; import StatusPageIcons from '@app/components/StatusPageIcons'; import useSession from '@app/utils/useSession'; @@ -48,10 +48,8 @@ import { getProvider, getDescription, formatTime, - getIsDisabled, getStatus, HIDDEN_LABELS_DETAIL_VIEW, - getIncidentUrl, formatString, getRating, CUSTOM_LABELS, @@ -83,16 +81,24 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo const { description, descriptionFormat } = getDescription(catalogItem); const lastSuccessfulProvisionTime = getLastSuccessfulProvisionTime(catalogItem); const helpLink = useHelpLink(); + const asset_uuid = catalogItem.metadata.labels?.['gpte.redhat.com/asset-uuid']; const { data: metrics } = useSWRImmutable( - catalogItem.metadata.labels?.['gpte.redhat.com/asset-uuid'] - ? apiPaths.ASSET_METRICS({ asset_uuid: catalogItem.metadata.labels['gpte.redhat.com/asset-uuid'] }) - : null, + asset_uuid ? apiPaths.ASSET_METRICS({ asset_uuid }) : null, fetcher, { shouldRetryOnError: false, suspense: false, - }, + } + ); + const { data: catalogItemIncident } = useSWR( + asset_uuid ? apiPaths.CATALOG_ITEM_LAST_INCIDENT({ namespace, asset_uuid }) : null, + fetcher ); + const catalogItemCpy = useMemo(() => { + const cpy = Object.assign({}, catalogItem); + cpy.metadata.annotations[`${BABYLON_DOMAIN}/incident`] = JSON.stringify(catalogItemIncident); + return cpy; + }, [catalogItem, catalogItemIncident]); const { data: userResourceClaims } = useSWR( userNamespace?.name ? apiPaths.RESOURCE_CLAIMS({ @@ -106,12 +112,12 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo namespace: userNamespace.name, limit: FETCH_BATCH_LIMIT, continueId, - }), + }) ), { refreshInterval: 8000, compare: compareK8sObjectsArr, - }, + } ); const services: ResourceClaim[] = useMemo( @@ -119,7 +125,7 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo Array.isArray(userResourceClaims) ? [].concat(...userResourceClaims.filter((r) => !isResourceClaimPartOfWorkshop(r))) : [], - [userResourceClaims], + [userResourceClaims] ); const descriptionHtml = useMemo( @@ -131,12 +137,10 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo }} /> ), - [description, descriptionFormat], + [description, descriptionFormat] ); - const isDisabled = getIsDisabled(catalogItem); - const { code: statusCode, name: statusName } = getStatus(catalogItem); - const incidentUrl = getIncidentUrl(catalogItem); + const status = getStatus(catalogItemCpy); const rating = getRating(catalogItem); const accessCheckResult = checkAccessControl(accessControl, groups, isAdmin); let autoStopTime = catalogItem.spec.runtime?.default; @@ -152,12 +156,12 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo isAdmin || isLabDeveloper(groups) ? CatalogItemAccess.Allow : accessCheckResult === 'deny' - ? CatalogItemAccess.Deny - : services.length >= 5 - ? CatalogItemAccess.Deny - : accessCheckResult === 'allow' - ? CatalogItemAccess.Allow - : CatalogItemAccess.RequestInformation; + ? CatalogItemAccess.Deny + : services.length >= 5 + ? CatalogItemAccess.Deny + : accessCheckResult === 'allow' + ? CatalogItemAccess.Allow + : CatalogItemAccess.RequestInformation; const catalogItemAccessDenyReason = catalogItemAccess !== CatalogItemAccess.Deny ? null : services.length >= 5 ? (

    @@ -231,7 +235,7 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo key="order-catalog-item" onClick={orderCatalogItem} variant="primary" - isDisabled={isAdmin ? false : isDisabled} + isDisabled={isAdmin ? false : status && status.disabled} className="catalog-item-details__main-btn" > Order{' '} @@ -252,28 +256,28 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo url={ new URL( `/catalog?item=${catalogItem.metadata.namespace}/${catalogItem.metadata.name}`, - window.location.origin, + window.location.origin ) } name={catalogItemName} /> - {statusCode && statusCode !== 'operational' ? ( + {status && status.name !== 'Operational' ? (

    ) : null} @@ -319,8 +323,8 @@ const CatalogItemDetails: React.FC<{ catalogItem: CatalogItem; onClose: () => vo {attr === CUSTOM_LABELS.ESTIMATED_COST.key ? null : attr === CUSTOM_LABELS.SLA.key - ? 'Service Level' - : formatString(attr)} + ? 'Service Level' + : formatString(attr)} {attr === CUSTOM_LABELS.RATING.key ? ( diff --git a/catalog/ui/src/app/Catalog/CatalogItemListItem.tsx b/catalog/ui/src/app/Catalog/CatalogItemListItem.tsx index 07466affc..e90dd3890 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemListItem.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemListItem.tsx @@ -6,15 +6,7 @@ import StatusPageIcons from '@app/components/StatusPageIcons'; import { displayName, renderContent, stripHtml } from '@app/util'; import StarRating from '@app/components/StarRating'; import CatalogItemIcon from './CatalogItemIcon'; -import { - formatString, - getDescription, - getIsDisabled, - getProvider, - getRating, - getStage, - getStatus, -} from './catalog-utils'; +import { formatString, getDescription, getProvider, getRating, getStage, getStatus } from './catalog-utils'; import './catalog-item-list-item.css'; @@ -25,9 +17,8 @@ const CatalogItemListItem: React.FC<{ catalogItem: CatalogItem }> = ({ catalogIt const { description, descriptionFormat } = getDescription(catalogItem); const provider = getProvider(catalogItem); const stage = getStage(catalogItem); - const isDisabled = getIsDisabled(catalogItem); const rating = getRating(catalogItem); - const { code: status } = getStatus(catalogItem); + const status = getStatus(catalogItem); if (!searchParams.has('item')) { if (namespace) { @@ -39,14 +30,14 @@ const CatalogItemListItem: React.FC<{ catalogItem: CatalogItem }> = ({ catalogIt return ( - {status && status !== 'operational' ? ( - + {status && status.name !== 'operational' ? ( + ) : null} {stage === 'dev' ? ( development diff --git a/catalog/ui/src/app/Catalog/catalog-utils.ts b/catalog/ui/src/app/Catalog/catalog-utils.ts index 70b56d0a7..fbc382b4e 100644 --- a/catalog/ui/src/app/Catalog/catalog-utils.ts +++ b/catalog/ui/src/app/Catalog/catalog-utils.ts @@ -1,6 +1,5 @@ -import { CatalogItem } from '@app/types'; +import { CatalogItem, CatalogItemIncident } from '@app/types'; import { BABYLON_DOMAIN, CATALOG_MANAGER_DOMAIN, formatDuration } from '@app/util'; -import { Ops } from '@app/Admin/CatalogItemAdmin'; export function getProvider(catalogItem: CatalogItem) { const { domain, key } = CUSTOM_LABELS.PROVIDER; @@ -25,7 +24,7 @@ export function getStage(catalogItem: CatalogItem) { } const supportedSLAs = ['Enterprise_Premium', 'Enterprise_Standard', 'Community', 'External_Support'] as const; -type SLAs = (typeof supportedSLAs)[number]; +type SLAs = typeof supportedSLAs[number]; export function getSLA(catalogItem: CatalogItem): SLAs { const { domain, key } = CUSTOM_LABELS.SLA; const sla = catalogItem.metadata.labels?.[`${domain}/${key}`] as SLAs; @@ -33,14 +32,6 @@ export function getSLA(catalogItem: CatalogItem): SLAs { return sla; } -export function getIsDisabled(catalogItem: CatalogItem): boolean { - const { domain, key } = CUSTOM_LABELS.DISABLED; - if (catalogItem.metadata.labels?.[`${domain}/${key}`]) { - return catalogItem.metadata.labels[`${domain}/${key}`] === 'true'; - } - return false; -} - export function getRating(catalogItem: CatalogItem): { ratingScore: number; totalRatings: number } | null { const { domain, key } = CUSTOM_LABELS.RATING; const ratingScoreSelector = catalogItem.metadata.labels?.[`${domain}/${key}`]; @@ -79,7 +70,7 @@ export function getLastSuccessfulProvisionTime(catalogItem: CatalogItem) { if (catalogItem.metadata.annotations?.[`${CATALOG_MANAGER_DOMAIN}/lastSuccessfulProvision`]) { const now = new Date(); const provisionDate = new Date( - catalogItem.metadata.annotations[`${CATALOG_MANAGER_DOMAIN}/lastSuccessfulProvision`], + catalogItem.metadata.annotations[`${CATALOG_MANAGER_DOMAIN}/lastSuccessfulProvision`] ); if (provisionDate < now) { return provisionDate.getTime(); @@ -89,26 +80,22 @@ export function getLastSuccessfulProvisionTime(catalogItem: CatalogItem) { return null; } export function getStatus( - catalogItem: CatalogItem, -): { code: string; name: string; updated?: { author: string; updatedAt: string } } | null { - if (catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`]) { - const ops: Ops = JSON.parse(catalogItem.metadata.annotations[`${BABYLON_DOMAIN}/ops`]); - if (ops.status.id) { - switch (ops.status.id) { - case 'degraded-performance': - return { code: ops.status.id, name: 'Degraded performance', updated: ops.status.updated }; - case 'partial-outage': - return { code: ops.status.id, name: 'Partial outage', updated: ops.status.updated }; - case 'major-outage': - return { code: ops.status.id, name: 'Major outage', updated: ops.status.updated }; - case 'under-maintenance': - return { code: ops.status.id, name: 'Under maintenance', updated: ops.status.updated }; - default: - return { code: 'operational', name: 'Operational', updated: ops.status.updated }; - } + catalogItem: CatalogItem +): { name: string; updated?: { author: string; updatedAt: string }; disabled: boolean; incidentUrl?: string } | null { + if (catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/incident`]) { + const catalog_incident: CatalogItemIncident = JSON.parse( + catalogItem.metadata.annotations[`${BABYLON_DOMAIN}/incident`] + ); + if (catalog_incident) { + return { + name: catalog_incident.status, + updated: { author: catalog_incident.created_by, updatedAt: catalog_incident.updated_at }, + disabled: catalog_incident.disabled, + incidentUrl: catalog_incident.incident_url, + }; } } - return { code: null, name: '' }; + return { name: 'Operational', disabled: false, incidentUrl: null }; } export function isAutoStopDisabled(catalogItem: CatalogItem) { @@ -118,14 +105,6 @@ export function isAutoStopDisabled(catalogItem: CatalogItem) { return false; } -export function getIncidentUrl(catalogItem: CatalogItem): string { - if (catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/ops`]) { - const ops: Ops = JSON.parse(catalogItem.metadata.annotations[`${BABYLON_DOMAIN}/ops`]); - return ops.incidentUrl || null; - } - return null; -} - export function formatTime(time: string): string { if (!time || time.length === 0) { return '-'; diff --git a/catalog/ui/src/app/api.ts b/catalog/ui/src/app/api.ts index 313d43b56..72f9e3be2 100644 --- a/catalog/ui/src/app/api.ts +++ b/catalog/ui/src/app/api.ts @@ -1611,6 +1611,12 @@ export const apiPaths: { [key in ResourceType]: (args: any) => string } = { `/apis/${BABYLON_DOMAIN}/v1/namespaces/${namespace}/catalogitems?limit=${limit}${ continueId ? `&continue=${continueId}` : '' }${labelSelector ? `&labelSelector=${labelSelector}` : ''}`, + CATALOG_ITEM_INCIDENTS: ({ namespace, asset_uuid }: { namespace: string; asset_uuid: string }) => + `/api/catalog_incident/incidents/${asset_uuid}/${namespace}`, + CATALOG_ITEM_LAST_INCIDENT: ({ namespace, asset_uuid }: { namespace: string; asset_uuid: string }) => + `/api/catalog_incident/last-incident/${asset_uuid}/${namespace}`, + CATALOG_ITEMS_ACTIVE_INCIDENT: ({ namespace }: { namespace?: string }) => + `/api/catalog_incident/active-incidents${namespace ? `?stage=${namespace}` : ''}`, RESOURCE_CLAIMS: ({ namespace, limit, diff --git a/catalog/ui/src/app/components/StatusPageIcons/index.tsx b/catalog/ui/src/app/components/StatusPageIcons/index.tsx index 294c0da08..476cb7649 100644 --- a/catalog/ui/src/app/components/StatusPageIcons/index.tsx +++ b/catalog/ui/src/app/components/StatusPageIcons/index.tsx @@ -10,13 +10,13 @@ const StatusPageIcons: React.FC<{ status: string } & React.HTMLAttributes { switch (status) { - case 'degraded-performance': + case 'Degraded performance': return ; - case 'partial-outage': + case 'Partial outage': return ; - case 'major-outage': + case 'Major outage': return ; - case 'under-maintenance': + case 'Under maintenance': return ; default: return ; diff --git a/catalog/ui/src/app/types.ts b/catalog/ui/src/app/types.ts index 408c891a2..e0fa681a2 100644 --- a/catalog/ui/src/app/types.ts +++ b/catalog/ui/src/app/types.ts @@ -112,6 +112,38 @@ export interface CatalogItem extends K8sObject { }; } +export type CatalogItemIncidentStatus = + | 'Degraded performance' + | 'Operational' + | 'Under maintenance' + | 'Partial outage' + | 'Major outage'; + +export interface CatalogItemIncident { + created_by: string; + disabled: boolean; + status: CatalogItemIncidentStatus; + incident_url: string; + jira_url: string; + comments: string; + asset_uuid: string; + stage: string; + active: boolean; + created_at: string; + downtime_interval: string; + downtime_hours: number; + last_incident_at: string; + resolved_at: string; + uptime_interval: string; + uptime_hours: number; + updated_at: string; + id: number; +} + +export interface CatalogItemIncidents { + items: CatalogItemIncident[]; +} + export interface CatalogItemList { items: CatalogItem[]; metadata: K8sObjectListMeta; @@ -578,6 +610,9 @@ export type ResourceType = | 'CATALOG_ITEM' | 'ASSET_METRICS' | 'CATALOG_ITEMS' + | 'CATALOG_ITEM_INCIDENTS' + | 'CATALOG_ITEM_LAST_INCIDENT' + | 'CATALOG_ITEMS_ACTIVE_INCIDENT' | 'RESOURCE_CLAIMS' | 'RESOURCE_CLAIM' | 'NAMESPACES'