diff --git a/src/components/ArtifactsList.tsx b/src/components/ArtifactsList.tsx index 8db188f1..4e5c608a 100644 --- a/src/components/ArtifactsList.tsx +++ b/src/components/ArtifactsList.tsx @@ -2,7 +2,7 @@ * This file is part of ciboard * * Copyright (c) 2023 Matěj Grabovský - * Copyright (c) 2023 Andrei Stepanov + * Copyright (c) 2023, 2024 Andrei Stepanov * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,23 +21,16 @@ import _ from 'lodash'; import { - Flex, - Text, - Title, Brand, Button, Spinner, - FlexItem, Bullseye, - TitleSizes, EmptyState, - TextContent, EmptyStateIcon, EmptyStateHeader, } from '@patternfly/react-core'; import { Td, Th, Tr, Table, Tbody, Thead } from '@patternfly/react-table'; import { Link } from 'react-router-dom'; -import { CSSProperties } from 'react'; import { ExclamationCircleIcon } from '@patternfly/react-icons'; import rhMbs from './../img/rhmbs.png'; @@ -45,11 +38,6 @@ import rhLogo from './../img/rhfavicon.svg'; import rhContLogo from './../img/rhcont.png'; import { useAppSelector } from '../hooks'; import { PaginationToolbar } from './PaginationToolbar'; -import { - resultColor, - isGatingArtifact, - GatingStatusIcon, -} from '../utils/utils'; import { Artifact, getAType, @@ -57,104 +45,16 @@ import { ArtifactMbs, isArtifactMbs, isArtifactRpm, + getArtifactId, + getArtifactRemoteUrl, getArtifactLocalPath, + getArtifactTypeLabel, ArtifactContainerImage, - GreenwaveDecisionReply, isArtifactRedhatContainerImage, - getArtifactTypeLabel, - getArtifactId, - getArtifactRemoteUrl, } from '../types'; import { ExternalLink } from './ExternalLink'; - -interface PrintRequirementsSizeProps { - allReqs: { [key: string]: number }; - reqName: string; -} - -const PrintRequirementsSize = (props: PrintRequirementsSizeProps) => { - const { reqName, allReqs } = props; - const color = resultColor(reqName); - const style: CSSProperties = { - color: `var(${color})`, - whiteSpace: 'nowrap', - }; - return ( - - {allReqs[reqName]} {reqName} - - ); -}; - -interface ArtifactGreenwaveStatesSummaryProps { - artifact: ArtifactRpm | ArtifactContainerImage | ArtifactMbs; -} -export const ArtifactGreenwaveStatesSummary: React.FC< - ArtifactGreenwaveStatesSummaryProps -> = (props) => { - const { artifact } = props; - const { isLoadingExtended: isLoading } = useAppSelector( - (state) => state.artifacts, - ); - const isScratch = _.get(artifact, 'hit_source.scratch', false); - if (isScratch) { - return <>scratch; - } - if (!isGatingArtifact(artifact)) { - return null; - } - const decision: GreenwaveDecisionReply | undefined = - artifact.greenwaveDecision; - if (_.isNil(decision) && !isLoading) { - return null; - } - - const reqSummary: { [name: string]: number } = {}; - /* - * Ignore the 'fetched-gating-yaml' virtual test as we dont display it in the UI. - * It is prevented from displaying in `ArtifactStatesList()`: - * `if (stateName === 'fetched-gating-yaml') continue;` - */ - const unsatisfiedCount = decision?.unsatisfied_requirements?.length; - const satisfiedCount = decision?.satisfied_requirements?.filter( - ({ type }) => type !== 'fetched-gating-yaml', - ).length; - if (unsatisfiedCount) { - reqSummary['err'] = unsatisfiedCount; - } - if (satisfiedCount) { - reqSummary['ok'] = satisfiedCount; - } - - const gatingPassed = decision?.policies_satisfied; - const iconStyle = { height: '1.2em' }; - const statusIcon = isLoading ? null : ( - - ); - - return ( - - - - {statusIcon} - - - {_.map(reqSummary, (_len, reqName) => ( - - - - ))} - {isLoading && ( - - - - )} - - ); -}; +import { store } from '../reduxStore'; +import { ArtifactGreenwaveStatesSummary } from './GatingStatus'; interface ShowLoadingProps {} function ShowLoading(props: ShowLoadingProps) { @@ -176,6 +76,7 @@ function ShowLoading(props: ShowLoadingProps) { } const makeArtifactRowRpm = (artifact: ArtifactRpm): ArtifactRow => { + const { isLoadingExtended: isLoading } = store.getState().artifacts; const { hit_source } = artifact; const aType = getAType(artifact); const href = getArtifactLocalPath(artifact); @@ -195,7 +96,10 @@ const makeArtifactRowRpm = (artifact: ArtifactRpm): ArtifactRow => { const cell3: JSX.Element = <>{hit_source.nvr}; const cell4: JSX.Element = ( <> - + ); const cell5: JSX.Element = <>{hit_source.gateTag}; @@ -223,6 +127,7 @@ const makeArtifactRowRpm = (artifact: ArtifactRpm): ArtifactRow => { const makeArtifactRowRedhatContainerImage = ( artifact: ArtifactContainerImage, ): ArtifactRow => { + const { isLoadingExtended: isLoading } = store.getState().artifacts; const { hit_source } = artifact; const aType = getAType(artifact); const href = getArtifactLocalPath(artifact); @@ -242,7 +147,10 @@ const makeArtifactRowRedhatContainerImage = ( const cell3: JSX.Element = <>{hit_source.nvr}; const cell4: JSX.Element = ( <> - + ); const cell5: JSX.Element = <>{hit_source.contTag}; @@ -268,6 +176,7 @@ const makeArtifactRowRedhatContainerImage = ( }; const makeArtifactRowMbs = (artifact: ArtifactMbs): ArtifactRow => { + const { isLoadingExtended: isLoading } = store.getState().artifacts; const { hit_source } = artifact; const aType = getAType(artifact); const href = getArtifactLocalPath(artifact); @@ -287,7 +196,10 @@ const makeArtifactRowMbs = (artifact: ArtifactMbs): ArtifactRow => { const cell3: JSX.Element = <>{hit_source.nsvc}; const cell4: JSX.Element = ( <> - + ); const cell5: JSX.Element = <>{hit_source.gateTag}; diff --git a/src/components/GatingStatus.tsx b/src/components/GatingStatus.tsx index 657c48f8..5090b12a 100644 --- a/src/components/GatingStatus.tsx +++ b/src/components/GatingStatus.tsx @@ -1,7 +1,7 @@ /* * This file is part of ciboard - * Copyright (c) 2022 Andrei Stepanov + * Copyright (c) 2022, 2024 Andrei Stepanov * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,20 +21,39 @@ import _ from 'lodash'; import React from 'react'; import { Flex, - Text, - Title, + Label, Spinner, FlexItem, - TitleSizes, - TextContent, + LabelProps, } from '@patternfly/react-core'; +import { GatingStatusIcon, isGatingArtifact } from '../utils/utils'; import { - resultColor, - GatingStatusIcon, - isGatingArtifact, -} from '../utils/utils'; -import { Artifact, getGwDecision, isArtifactScratch } from '../types'; + Artifact, + getGwDecision, + AChildGreenwave, + isArtifactScratch, + GreenwaveRequirementTypes, +} from '../types'; +import { mkReqStatesGreenwave } from '../utils/stages_states'; + +type ColorPropType = Exclude; + +export const resultColors: Record = { + green: ['passed'], + red: ['errored', 'failed'], + orange: ['missing', 'needs inspection'], + + cyan: ['waived'], + blue: ['running'], + purple: ['queued', 'skip'], + gold: ['info'], + grey: [], +}; + +export const resultColor = (result: string) => { + return _.findKey(resultColors, (item) => item.indexOf(result) !== -1); +}; interface PrintRequirementsSizeProps { allReqs: { [key: string]: number }; @@ -43,20 +62,52 @@ interface PrintRequirementsSizeProps { const PrintRequirementsSize = (props: PrintRequirementsSizeProps) => { const { reqName, allReqs } = props; - const color = resultColor(reqName); - const style = { color: `var(${color})` }; + const color: ColorPropType = + (resultColor(reqName) as ColorPropType | undefined) || 'grey'; return ( - + <Label variant="outline" color={color}> {allReqs[reqName]} {reqName} - + ); }; -interface PrintRequirementsSizeProps { - allReqs: { [key: string]: number }; - reqName: string; -} +// https://pagure.io/fedora-ci/messages/blob/master/f/schemas/test-complete.yaml#_14 + +const gwStateMappings: Record = { + excluded: { default: 'excluded' }, + blacklisted: { default: 'blacklisted' }, + 'test-result-failed': { + needs_inspection: 'needs inspection', + default: 'failed', + }, + 'test-result-passed': { + default: 'passed', + passed: 'passed', + info: 'info', + not_applicable: 'not applicable', + }, + 'test-result-missing': { + running: 'running', + queued: 'queued', + default: 'missing', + }, + 'test-result-errored': { default: 'errored' }, + 'invalid-gating-yaml': { default: 'invalid gating.yaml' }, + // Do not display + 'fetched-gating-yaml': { default: 'fetched gating.yaml' }, + 'missing-gating-yaml': { default: 'missing gating.yaml' }, + 'failed-fetch-gating-yaml': { default: 'fail fetch gating.yaml' }, + 'invalid-gating-yaml-waived': { default: 'invalid gating.yaml waived' }, + 'missing-gating-yaml-waived': { default: 'missing gating.yaml waived' }, + 'test-result-failed-waived': { default: 'failed waived' }, + 'test-result-missing-waived': { default: 'missing waived' }, + 'test-result-errored-waived': { default: 'erroredd waived' }, + 'failed-fetch-gating-yaml-waived': { + default: 'fail fetch gating.yaml waived', + }, +}; +// artifact: ArtifactRpm | ArtifactContainerImage | ArtifactMbs; interface ArtifactGreenwaveStatesSummaryProps { artifact: Artifact; isLoading?: boolean; @@ -66,58 +117,75 @@ export const ArtifactGreenwaveStatesSummary: React.FC< ArtifactGreenwaveStatesSummaryProps > = (props) => { const { artifact, isLoading } = props; - if (!isGatingArtifact(artifact)) { - return null; - } - const decision = getGwDecision(artifact); const isScratch = isArtifactScratch(artifact); if (isScratch) { return null; } - if (_.isNil(decision) && !isLoading) { + if (!isGatingArtifact(artifact)) { + return null; + } + if (isLoading) { + return ; + } + const decision = getGwDecision(artifact); + if (!decision) { return null; } const reqSummary: { [name: string]: number } = {}; - /* - * Ignore the 'fetched-gating-yaml' virtual test as we dont display it in the UI. - * It is prevented from displaying in `ArtifactStatesList()`: - * `if (stateName === 'fetched-gating-yaml') continue;` - */ - const unsatisfiedCount = decision?.unsatisfied_requirements?.length; - const satisfiedCount = decision?.satisfied_requirements?.filter( - ({ type }) => type !== 'fetched-gating-yaml', - ).length; - if (unsatisfiedCount) { - reqSummary['err'] = unsatisfiedCount; + const reqStatesGreenwave = mkReqStatesGreenwave(decision); + let totalWaivers = 0; + for (const stateName of _.keys( + reqStatesGreenwave, + ) as GreenwaveRequirementTypes[]) { + if (reqStatesGreenwave.hasOwnProperty(stateName)) { + if (stateName === 'fetched-gating-yaml') { + /* + * Ignore the 'fetched-gating-yaml' virtual test as we dont display it in the UI. + * It is prevented from displaying in `ArtifactStatesList()`: + */ + continue; + } + const gwStates = reqStatesGreenwave[stateName] as AChildGreenwave[]; + // stateName === requirement.type + const namingRules = gwStateMappings[stateName]; + for (const state of gwStates) { + const resultOutcome = state.result?.outcome; + const reqName = resultOutcome + ? _.get( + namingRules, + _.toLower(resultOutcome), + resultOutcome, + ) + : namingRules['default']; + reqSummary[reqName] = reqSummary[reqName] + ? reqSummary[reqName] + 1 + : 1; + const waived = !!state.waiver; + if (waived) { + totalWaivers++; + } + } + } } - if (satisfiedCount) { - reqSummary['ok'] = satisfiedCount; + if (totalWaivers) { + reqSummary['waived'] = totalWaivers; } const gatingPassed = decision?.policies_satisfied; const iconStyle = { height: '1.2em' }; - const statusIcon = isLoading ? null : ( + const statusIcon = ( ); return ( - - - {statusIcon} - - + {statusIcon} {_.map(reqSummary, (_len, reqName) => ( - + ))} - {isLoading && ( - - - - )} ); }; diff --git a/src/components/PageDetails/artifactTests.ts b/src/components/PageDetails/artifactTests.ts index 403a6ec2..feaf96f1 100644 --- a/src/components/PageDetails/artifactTests.ts +++ b/src/components/PageDetails/artifactTests.ts @@ -328,6 +328,9 @@ export function extractTests( metadata: MetadataRaw[], ): CiTest[] { const stagesStates = mkStagesAndStates(artifact); + console.log( + 'FIX ME!!!!!! For GW artifacts it does not have DB tests, and otherwise applies too', + ); const tests = stagesStates.flatMap(([_stage, stateName, tests]) => tests.map((aChild) => transformTest(artifact, aChild, stateName, metadata), diff --git a/src/types.ts b/src/types.ts index db09fcf7..5e392073 100644 --- a/src/types.ts +++ b/src/types.ts @@ -625,23 +625,28 @@ export type HitSourceArtifactCompose = { * Decision requirements types * https://gating-greenwave.readthedocs.io/en/latest/decision_requirements.html */ + +const greenwaveRequirementTypesArray = [ + 'excluded', + 'blacklisted', + 'test-result-failed', // NEEDS_INSPECTION + 'test-result-passed', + 'test-result-missing', + 'test-result-errored', + 'invalid-gating-yaml', + 'fetched-gating-yaml', + 'missing-gating-yaml', + 'failed-fetch-gating-yaml', + 'invalid-gating-yaml-waived', + 'missing-gating-yaml-waived', + 'test-result-failed-waived', + 'test-result-missing-waived', + 'test-result-errored-waived', + 'failed-fetch-gating-yaml-waived', +] as const; + export type GreenwaveRequirementTypes = - | 'excluded' - | 'blacklisted' - | 'test-result-failed' - | 'test-result-passed' - | 'test-result-missing' - | 'test-result-errored' - | 'invalid-gating-yaml' - | 'fetched-gating-yaml' - | 'missing-gating-yaml' - | 'failed-fetch-gating-yaml' - | 'invalid-gating-yaml-waived' - | 'missing-gating-yaml-waived' - | 'test-result-failed-waived' - | 'test-result-missing-waived' - | 'test-result-errored-waived' - | 'failed-fetch-gating-yaml-waived'; + (typeof greenwaveRequirementTypesArray)[number]; /** * Opposite to test-messages child, greenwave/resultsdb state diff --git a/src/utils/artifactsTable.tsx b/src/utils/artifactsTable.tsx index 704a6a15..4fea3130 100644 --- a/src/utils/artifactsTable.tsx +++ b/src/utils/artifactsTable.tsx @@ -20,302 +20,10 @@ import _ from 'lodash'; import React from 'react'; -import { LegacyRef, useState } from 'react'; -import { - List, - Text, - Title, - Spinner, - Bullseye, - ListItem, - OrderType, - EmptyState, - TextContent, - TextVariants, - ListComponent, - EmptyStateBody, - EmptyStateIcon, - EmptyStateVariant, - ExpandableSection, - EmptyStateHeader, -} from '@patternfly/react-core'; -import { - DropdownProps, - DropdownToggleProps, -} from '@patternfly/react-core/deprecated'; -import { RowWrapperProps, IRow } from '@patternfly/react-table'; -import { TableProps } from '@patternfly/react-table/deprecated'; -import { ExclamationCircleIcon, LinkIcon } from '@patternfly/react-icons'; -import { global_danger_color_200 as globalDangerColor200 } from '@patternfly/react-tokens'; +import { DropdownProps } from '@patternfly/react-core/deprecated'; -import styles from '../custom.module.css'; -import { - Artifact, - getArtifactId, - getArtifactName, - getArtifactRemoteUrl, -} from '../types'; -import { ArtifactGreenwaveStatesSummary } from '../components/GatingStatus'; - -export interface ArtifactNameProps { - artifact: Artifact; -} - -export const ArtifactName: React.FC = ({ artifact }) => { - return ( - - - {getArtifactName(artifact) || - 'Unknown artifact, please file a bug'} - - - ); -}; - -export interface ArtifactDestinationProps { - artifact: Artifact; -} - -export const ArtifactDestination: React.FC = ( - props, -) => { - const { artifact } = props; - const gating_tag: string | undefined = _.get( - artifact, - 'payload.gate_tag_name', - ); - if (gating_tag) { - return ( - - {gating_tag} - - ); - } - if (_.get(artifact, 'payload.scratch')) { - return ( - - scratch - - ); - } - return null; -}; - -export interface ArtifactUrlProps { - artifact: Artifact; -} - -export const ArtifactUrl: React.FC = (props) => { - const { artifact } = props; - const url = getArtifactRemoteUrl(artifact); - const aid = getArtifactId(artifact); - return ( - - - e.stopPropagation()} - > - {aid} - - - - ); -}; - -const artifactDashboardUrl = (artifact: any) => { - return `${window.location.origin}/#/artifact/${artifact.type}/aid/${artifact.aid}`; -}; - -type CustomRowWrapperPropsWithChildren = - React.PropsWithChildren & { - scrollRef: LegacyRef; - }; - -export const CustomRowWrapper = ( - props: CustomRowWrapperPropsWithChildren, -): JSX.Element => { - const { row, children, scrollRef } = props; - if (!(children instanceof Array)) { - throw new Error('Expect array of rows'); - } - const firstCell: React.ReactNode = children[0]; - if (!React.isValidElement(firstCell)) { - throw new Error('Child must be valid element'); - } - const isOpenParent: boolean = firstCell.props.children.props.isOpen; - const ref = isOpenParent ? scrollRef : undefined; - return ( - - ); -}; - -export const ShowErrors = ({ error, forceExpand }: any) => { - const [isExpanded, setExpanded] = useState(false); - if (!error) { - return null; - } - const errors = []; - errors.push(${error.toString()}); - if ( - error.networkError && - error.networkError.result && - !_.isEmpty(error.networkError.result.errors) - ) { - _.forEach(error.networkError.result.errors, ({ message }, i) => { - errors.push({message}); - }); - } - if (error.graphQLErrors) { - _.forEach(error.graphQLErrors, ({ message, locations, path }, i) => - errors.push( - {`${message}, ${locations}, ${path}`}, - ), - ); - } - const onToggle = (isExpanded: boolean) => { - setExpanded(isExpanded); - }; - let toggleText = 'Show errors'; - if (forceExpand) { - toggleText = ''; - } else if (isExpanded) { - toggleText = 'Hide errors'; - } - return ( - - - - onToggle(isExpanded) - } - isExpanded={isExpanded} - > - - {errors} - - - - - ); -}; - -export interface InputRowType { - body: React.ReactNode; - title: string; - type: string; -} - -export type OnCollapseEventType = TableProps['onCollapse']; -export type TableRowWrapperType = TableProps['rowWrapper']; -export type OnDropdownToggleType = DropdownToggleProps['onToggle']; export type OnDropdownSelectType = DropdownProps['onSelect']; -export const mkSpecialRows = (args: InputRowType): IRow[] => { - const default_args = { type: 'error' }; - const { title, body, type }: any = { ...default_args, ...args }; - let Icon = () => <>; - if (type === 'error') { - Icon = () => ( - - ); - } else if (type === 'loading') { - Icon = () => ; - } - return [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 8 }, - title: ( -
- - - - {title}} - headingLevel="h2" - /> - {body} - - -
- ), - }, - ], - }, - ]; -}; - -export const mkArtifactRow = ( - artifact: Artifact, - gatingDecisionIsLoading: boolean, -): IRow => { - const packager: string = _.get( - artifact, - 'payload.issuer', - 'Unknown packager', - ); - const cells = [ - { - title: , - }, - { - title: , - }, - { - title: ( - - ), - }, - { - title: , - }, - { - title:
{packager}
, - }, - { - title: ( - - - - ), - }, - ]; - return { cells, isOpen: false }; -}; - -export type InputArtifactRowType = { - artifacts: Artifact[]; - opened: number | null; - body?: JSX.Element; - waitForRef?: React.MutableRefObject; - gatingDecisionIsLoading: boolean; -}; - export function mkSeparatedList( elements: React.ReactNode[], separator: React.ReactNode = ', ', diff --git a/src/utils/stages_states.ts b/src/utils/stages_states.ts index 4df4c599..6ac263c2 100644 --- a/src/utils/stages_states.ts +++ b/src/utils/stages_states.ts @@ -84,7 +84,6 @@ export const mkStagesAndStates = ( const stagesStates: Array = []; // Preprocess broker-messages into a list sorted by stage and state. const testMsgStagesStates = aChildrenByStageName(artifact); - console.log('test stage_states', testMsgStagesStates); stagesStates.push(...testMsgStagesStates); /* * Greenwave always produces structured-reply, check if this reply makes any sense. @@ -279,7 +278,7 @@ const mkGreenwaveStageStates = ( return { msgStageName: 'greenwave', aChildrenByStateName }; }; -const mkReqStatesGreenwave = ( +export const mkReqStatesGreenwave = ( decision: GreenwaveDecisionReply, ): AChildrenByStateName => { const { satisfied_requirements, unsatisfied_requirements } = decision; diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index ea1aa82a..c67dc3fc 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -146,42 +146,6 @@ export const getOSVersionFromNvr = (nvr: string, artifactType: string) => { return osVersion; }; -export const resultColors = { - '--pf-v5-global--success-color--100': [ - 'complete', - 'passed', - 'pass', - 'Pass', - 'PASS', - true, - 'ok', - 'OK', - 'Ok', - 'satisfied', - ], - '--pf-v5-global--danger-color--100': [ - 'failed', - 'fail', - 'Fail', - 'FAIL', - 'missing', - false, - 'Err', - 'err', - 'error', - 'Error', - 'unsatisfied', - ], - '--pf-v5-global--warning-color--100': ['error', 'waived'], - '--pf-v5-global--link--Color': ['running'], - '--pf-v5-global--warning-color--200': ['queued', 'skip'], - '--pf-v5-global--info-color--100': ['info'], -}; - -export const resultColor = (result: string) => { - return _.findKey(resultColors, (item) => item.indexOf(result) !== -1); -}; - export const getMessageError = (brokerMsgBody: BrokerSchemaMsgBody) => { if (MSG_V_0_1.isMsg(brokerMsgBody) && 'reason' in brokerMsgBody) { return {