diff --git a/console-extensions.json b/console-extensions.json index f6d6a73f..23eda23c 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -69,6 +69,34 @@ "abbr": "PL" } }, + { + "type": "console.model-metadata", + "properties": { + "model": { + "group": "tekton.dev", + "version": "v1beta1", + "kind": "PipelineRun" + }, + "color": "#38812f", + "label": "%PipelineRun%", + "labelPlural": "%PipelineRuns%", + "abbr": "PLR" + } + }, + { + "type": "console.model-metadata", + "properties": { + "model": { + "group": "tekton.dev", + "version": "v1", + "kind": "PipelineRun" + }, + "color": "#38812f", + "label": "%PipelineRun%", + "labelPlural": "%PipelineRuns%", + "abbr": "PLR" + } + }, { "type": "console.page/route", "properties": { @@ -222,6 +250,48 @@ "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINES_LIST"] } }, + { + "type": "console.page/resource/list", + "properties": { + "model": { + "group": "tekton.dev", + "version": "v1beta1", + "kind": "PipelineRun" + }, + "component": { "$codeRef": "pipelineRunsList.PipelineRunsListPage" } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINERUNS_LIST"] + } + }, + { + "type": "console.page/resource/list", + "properties": { + "model": { + "group": "tekton.dev", + "version": "v1", + "kind": "PipelineRun" + }, + "component": { "$codeRef": "pipelineRunsList.PipelineRunsListPage" } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINERUNS_LIST"] + } + }, + { + "type": "console.page/resource/list", + "properties": { + "model": { + "group": "pipelinesascode.tekton.dev", + "version": "v1alpha1", + "kind": "Repository" + }, + "component": { "$codeRef": "repositoriesList.RepositoriesListPage" } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_REPOSITORIES_LIST"] + } + }, { "type": "console.page/resource/list", "properties": { @@ -487,6 +557,19 @@ "required": ["HIDE_STATIC_PIPELINE_PLUGIN_CLUSTERTRIGGERSBINDINGS_LIST"] } }, + { + "type": "console.page/route", + "properties": { + "path": ["/dev-pipelines/ns/:ns", "/dev-pipelines/all-namespaces"], + "exact": false, + "component": { + "$codeRef": "pipelinesList.PipelinesTabbedPage" + } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_NAV_OPTION"] + } + }, { "type": "console.page/route", "properties": { @@ -500,6 +583,19 @@ "required": ["HIDE_STATIC_PIPELINE_PLUGIN_TRIGGERS_NAV_OPTION"] } }, + { + "type": "console.page/route", + "properties": { + "path": ["/pipelines/ns/:ns", "/pipelines/all-namespaces"], + "exact": false, + "component": { + "$codeRef": "pipelinesList.PipelinesTabbedPage" + } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_NAV_OPTION"] + } + }, { "type": "console.navigation/href", "properties": { @@ -606,5 +702,42 @@ "flags": { "required": ["HIDE_STATIC_PIPELINE_PLUGIN_TASKRUN_DETAILS"] } + }, + { + "type": "console.navigation/href", + "properties": { + "id": "pipelines", + "perspective": "admin", + "section": "pipelines", + "name": "%Pipelines%", + "href": "/pipelines", + "insertBefore": "pipeline-tasks", + "namespaced": true, + "dataAttributes": { + "data-quickstart-id": "qs-nav-pipelines" + } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_NAV_OPTION"] + } + }, + { + "type": "console.navigation/href", + "properties": { + "id": "pipelines", + "perspective": "dev", + "section": "resources", + "insertAfter": "builds", + "name": "%Pipelines%", + "href": "/dev-pipelines", + "namespaced": true, + "dataAttributes": { + "data-quickstart-id": "qs-nav-pipelines", + "data-test-id": "pipeline-header" + } + }, + "flags": { + "required": ["HIDE_STATIC_PIPELINE_PLUGIN_PIPELINE_NAV_OPTION"] + } } ] diff --git a/locales/en/plugin__pipelines-console-plugin.json b/locales/en/plugin__pipelines-console-plugin.json index d429958a..de92902e 100644 --- a/locales/en/plugin__pipelines-console-plugin.json +++ b/locales/en/plugin__pipelines-console-plugin.json @@ -28,6 +28,7 @@ "Archived in Tekton results": "Archived in Tekton results", "Average duration": "Average duration", "Average Duration": "Average Duration", + "Cancel": "Cancel", "Cancelled": "Cancelled", "Cancelling": "Cancelling", "ClusterTask": "ClusterTask", @@ -45,11 +46,11 @@ "Create": "Create", "Create {{name}}": "Create {{name}}", "Create {{resourceKind}}": "Create {{resourceKind}}", - "Create Pipeline": "Create Pipeline", "Created": "Created", "Created at": "Created at", "Critical": "Critical", "Delete {{resourceKind}}": "Delete {{resourceKind}}", + "Delete PipelineRun": "Delete PipelineRun", "Details": "Details", "Download": "Download", "Download all": "Download all", @@ -68,6 +69,7 @@ "Error loading events": "Error loading events", "Event": "Event", "Event stream is paused.": "Event stream is paused.", + "Event type": "Event type", "EventListener": "EventListener", "EventListeners": "EventListeners", "Events": "Events", @@ -79,16 +81,19 @@ "Generated from {{ sourceComponent }} on {{ sourceHost }}": "Generated from {{ sourceComponent }} on {{ sourceHost }}", "Generated from {{sourceComponent}} on <4>{{sourceHost}}": "Generated from {{sourceComponent}} on <4>{{sourceHost}}", "High": "High", + "Interrupt any executing non finally tasks, then execute finally tasks": "Interrupt any executing non finally tasks, then execute finally tasks", "Labels": "Labels", "Last day": "Last day", "Last month": "Last month", "Last quarter": "Last quarter", "Last run": "Last run", + "Last run duration": "Last run duration", "Last run status": "Last run status", "Last run time": "Last run time", "Last weeks": "Last weeks", "Last year": "Last year", "less than a sec": "less than a sec", + "Let the running tasks complete, then execute finally tasks": "Let the running tasks complete, then execute finally tasks", "Loading events...": "Loading events...", "Log snippet": "Log snippet", "Logs": "Logs", @@ -108,6 +113,8 @@ "No matching events": "No matching events", "No owner": "No owner", "No PipelineRuns found": "No PipelineRuns found", + "No Pipelines found": "No Pipelines found", + "No Repositories found": "No Repositories found", "No selector": "No selector", "No TaskRuns found": "No TaskRuns found", "No Tasks found": "No Tasks found", @@ -145,6 +152,7 @@ "Refresh off": "Refresh off", "Repositories": "Repositories", "Repository": "Repository", + "Rerun": "Rerun", "Resource is being fetched from Tekton Results.": "Resource is being fetched from Tekton Results.", "Route": "Route", "Routes": "Routes", @@ -161,10 +169,12 @@ "Showing {{count}} event_plural": "Showing {{count}} event", "Showing most recent {{count}} event": "Showing most recent {{count}} event", "Showing most recent {{count}} event_plural": "Showing most recent {{count}} event", + "Signed": "Signed", "Skipped": "Skipped", "Start streaming events": "Start streaming events", "Started": "Started", "Status": "Status", + "Stop": "Stop", "Streaming events...": "Streaming events...", "Succeeded": "Succeeded", "Success rate": "Success rate", @@ -194,6 +204,7 @@ "Value": "Value", "View logs": "View logs", "VolumeClaimTemplate Resources": "VolumeClaimTemplate Resources", + "Vulnerabilities": "Vulnerabilities", "Workspace Resources": "Workspace Resources", "Workspaces": "Workspaces", "YAML": "YAML" diff --git a/package.json b/package.json index 62bec852..326e3198 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,8 @@ "hookProvider": "./components/hooks", "pipelinesComponent": "./components/pipelines-overview", "pipelinesList": "./components/pipelines-list", + "pipelineRunsList": "./components/pipelineRuns-list", + "repositoriesList": "./components/repositories-list", "metricsComponent": "./components/pipelines-metrics", "tasksComponent": "./components/pipelines-tasks", "triggersDetails": "./components/triggers-details", diff --git a/src/components/hooks/useTektonResult.ts b/src/components/hooks/useTektonResult.ts index 5e881481..86df2724 100644 --- a/src/components/hooks/useTektonResult.ts +++ b/src/components/hooks/useTektonResult.ts @@ -1,4 +1,5 @@ import { + getGroupVersionKindForModel, K8sResourceCommon, Selector, useK8sWatchResource, @@ -10,13 +11,14 @@ import { RepositoryLabels, TektonResourceLabel, } from '../../consts'; +import { PipelineRunModel } from '../../models'; import { PipelineRunKind, TaskRunKind } from '../../types'; import { - RecordsList, - TektonResultsOptions, getPipelineRuns, getTaskRunLog, getTaskRuns, + RecordsList, + TektonResultsOptions, } from '../utils/tekton-results'; import { useTaskRuns } from './useTaskRuns'; @@ -126,7 +128,7 @@ export const useTRTaskRuns = ( export const useGetPipelineRuns = ( ns: string, options?: { name: string; kind: string }, -) => { +): [PipelineRunKind[], boolean, unknown, GetNextPage] => { let selector: Selector; if (options?.kind === 'Pipeline') { @@ -139,18 +141,17 @@ export const useGetPipelineRuns = ( }, }; } - const [resultPlrs, resultPlrsLoaded, resultPlrsLoadError, getNextPage] = - useTRPipelineRuns( - ns, - options && { - selector, - }, - ); + const [resultPlrs, resultPlrsLoaded, , getNextPage] = useTRPipelineRuns( + ns, + options && { + selector, + }, + ); const [k8sPlrs, k8sPlrsLoaded, k8sPlrsLoadError] = useK8sWatchResource< PipelineRunKind[] >({ isList: true, - kind: 'PipelineRun', + groupVersionKind: getGroupVersionKindForModel(PipelineRunModel), namespace: ns, ...(options ? { selector } : {}), }); @@ -158,12 +159,7 @@ export const useGetPipelineRuns = ( (resultPlrsLoaded || k8sPlrsLoaded) && !k8sPlrsLoadError ? uniqBy([...k8sPlrs, ...resultPlrs], (r) => r.metadata.name) : []; - return [ - mergedPlrs, - resultPlrsLoaded || k8sPlrsLoaded, - resultPlrsLoadError || k8sPlrsLoadError, - getNextPage, - ]; + return [mergedPlrs, k8sPlrsLoaded, k8sPlrsLoadError, getNextPage]; }; export const useGetTaskRuns = ( diff --git a/src/components/list-pages/ListPageCreateButton.tsx b/src/components/list-pages/ListPageCreateButton.tsx index 2326d13b..13bbc043 100644 --- a/src/components/list-pages/ListPageCreateButton.tsx +++ b/src/components/list-pages/ListPageCreateButton.tsx @@ -15,6 +15,22 @@ type ListPageCreateButtonProps = { hideTitle: boolean; }; +const getCreateLink = (model: K8sKind, namespace: string) => { + if (model.kind === 'Pipeline') { + return `/k8s/ns/${namespace || 'default'}/${getReferenceForModel( + model, + )}/~new/builder`; + } + if (model.kind === 'Repository') { + return `/k8s/ns/${namespace || 'default'}/${getReferenceForModel( + model, + )}/~new/form`; + } + return `/k8s${ + namespace ? `/ns/${namespace}` : `/cluster` + }/${getReferenceForModel(model)}/~new`; +}; + const ListPageCreateButton: React.FC = ({ model, namespace, @@ -32,9 +48,7 @@ const ListPageCreateButton: React.FC = ({ groupVersionKind: getGroupVersionKindForModel(model), namespace, }} - to={`/k8s${ - namespace ? `/ns/${namespace}` : `/cluster` - }/${getReferenceForModel(model)}/~new`} + to={getCreateLink(model, namespace)} > {t('Create {{name}}', { name: model.label })} diff --git a/src/components/list-pages/PipelinesTabbedPage.tsx b/src/components/list-pages/PipelinesTabbedPage.tsx new file mode 100644 index 00000000..5f7d4cd6 --- /dev/null +++ b/src/components/list-pages/PipelinesTabbedPage.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + NamespaceBar, + NavPage, + useFlag, +} from '@openshift-console/dynamic-plugin-sdk'; +import { PipelineModel, PipelineRunModel, RepositoryModel } from '../../models'; +import { RepositoriesList } from '../repositories-list'; +import { PipelinesList } from '../pipelines-list'; +import { PipelineRunsList } from '../pipelineRuns-list'; +import { MenuAction, MenuActions, MultiTabListPage } from '../multi-tab-list'; +import { FLAG_OPENSHIFT_PIPELINE_AS_CODE } from '../../consts'; + +export const PageContents: React.FC = () => { + const { t } = useTranslation(); + const isRepositoryEnabled = useFlag(FLAG_OPENSHIFT_PIPELINE_AS_CODE); + + const menuActions: MenuActions = { + pipeline: { + model: PipelineModel, + onSelection: (key: string, action: MenuAction, url: string) => + `${url}/builder`, + }, + pipelineRun: { model: PipelineRunModel }, + repository: { + model: RepositoryModel, + onSelection: (_key: string, _action: MenuAction, url: string) => + `${url}/form`, + }, + }; + const pages: NavPage[] = [ + { + href: '', + // t(PipelineModel.labelPluralKey) + name: PipelineModel.labelPluralKey, + component: PipelinesList, + }, + { + href: 'pipeline-runs', + // t(PipelineRunModel.labelPluralKey) + name: PipelineRunModel.labelPluralKey, + component: PipelineRunsList, + }, + ...(isRepositoryEnabled + ? [ + { + href: 'repositories', + // t(RepositoryModel.labelPluralKey) + name: RepositoryModel.labelPluralKey, + component: RepositoriesList, + }, + ] + : []), + ]; + + // return namespace ? ( + // + // ) : ( + // + // {(openProjectModal) => ( + // + // Select a Project to view its details + // . + // + // )} + // + // ); + return ( + + ); +}; + +// const PageContentsWithStartGuide = withStartGuide(PageContents); + +const PipelinesTabbedPage: React.FC = (props) => { + return ( + <> + + + + ); +}; + +export default PipelinesTabbedPage; diff --git a/src/components/multi-tab-list/MultiTabListPage.scss b/src/components/multi-tab-list/MultiTabListPage.scss new file mode 100644 index 00000000..76ca205f --- /dev/null +++ b/src/components/multi-tab-list/MultiTabListPage.scss @@ -0,0 +1,3 @@ +.virtualized-table-empty-msg { + margin-top: var(--pf-global--spacer--md); +} diff --git a/src/components/multi-tab-list/MultiTabListPage.tsx b/src/components/multi-tab-list/MultiTabListPage.tsx index c0dc357e..6cc648ab 100644 --- a/src/components/multi-tab-list/MultiTabListPage.tsx +++ b/src/components/multi-tab-list/MultiTabListPage.tsx @@ -15,6 +15,7 @@ import { NavPage, } from '@openshift-console/dynamic-plugin-sdk'; import { PageTitleContext } from './PageTitleContext'; +import './MultiTabListPage.scss'; interface MultiTabListPageProps { title: string; diff --git a/src/components/pipelineRuns-list/PipelineRunsKebab.tsx b/src/components/pipelineRuns-list/PipelineRunsKebab.tsx new file mode 100644 index 00000000..eafb43aa --- /dev/null +++ b/src/components/pipelineRuns-list/PipelineRunsKebab.tsx @@ -0,0 +1,225 @@ +import * as React from 'react'; +import { PipelineRunKind, TaskRunKind } from '../../types'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import { + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, +} from '@patternfly/react-core'; +import { KEBAB_BUTTON_ID } from '../../consts'; +import { useTranslation } from 'react-i18next'; +import { + k8sCreate, + k8sPatch, + useAccessReview, + useDeleteModal, +} from '@openshift-console/dynamic-plugin-sdk'; +import { PipelineRunModel } from '../../models'; +import { returnValidPipelineRunModel } from '../utils/pipeline-utils'; +import { getPipelineRunData } from '../utils/utils'; +import { getTaskRunsOfPipelineRun } from '../hooks/useTaskRuns'; +import { + shouldHidePipelineRunCancel, + shouldHidePipelineRunStop, +} from '../utils/pipeline-augment'; +import { + isResourceLoadedFromTR, + tektonResultsFlag, +} from '../utils/common-utils'; + +type PipelineRunsKebabProps = { + obj: PipelineRunKind; + taskRuns: TaskRunKind[]; +}; + +const PipelineRunsKebab: React.FC = ({ + obj, + taskRuns, +}) => { + const { t } = useTranslation(); + const message = ( +

+ {t( + 'This action will delete resource from k8s but still the resource can be fetched from Tekton Results', + )} +

+ ); + + const launchDeleteModal = + !isResourceLoadedFromTR(obj) && tektonResultsFlag(obj) + ? useDeleteModal(obj, undefined, message) + : useDeleteModal(obj); + const [isOpen, setIsOpen] = React.useState(false); + const { name, namespace } = obj.metadata; + const PLRTasks = getTaskRunsOfPipelineRun(taskRuns, name); + const onToggle = () => { + setIsOpen(!isOpen); + }; + + const onSelect = () => { + setIsOpen(false); + }; + + const canCreateResource = useAccessReview({ + group: PipelineRunModel.apiGroup, + resource: PipelineRunModel.plural, + verb: 'create', + name, + namespace, + }); + + const canEditResource = useAccessReview({ + group: PipelineRunModel.apiGroup, + resource: PipelineRunModel.plural, + verb: 'update', + name, + namespace, + }); + const canDeleteResource = useAccessReview({ + group: PipelineRunModel.apiGroup, + resource: PipelineRunModel.plural, + verb: 'delete', + name, + namespace, + }); + + const reRunAction = () => { + const { pipelineRef, pipelineSpec } = obj.spec; + if (namespace && (pipelineRef?.name || pipelineSpec)) { + k8sCreate({ + model: returnValidPipelineRunModel(obj), + data: getPipelineRunData(null, obj), + }); + } else { + // errorModal({ + // error: i18n.t( + // 'pipelines-plugin~Invalid PipelineRun configuration, unable to start Pipeline.', + // ), + // }); + console.log( + 'Invalid PipelineRun configuration, unable to start Pipeline.', + ); + } + }; + + const cancelAction = () => { + k8sPatch({ + model: PipelineRunModel, + resource: { + metadata: { + name, + namespace, + }, + }, + data: [ + { + op: 'replace', + path: `/spec/status`, + value: 'CancelledRunFinally', + }, + ], + }); + }; + + const stopAction = () => { + k8sPatch({ + model: PipelineRunModel, + resource: { + metadata: { name, namespace }, + }, + data: [ + { + op: 'replace', + path: `/spec/status`, + value: 'StoppedRunFinally', + }, + ], + }); + }; + + const dropdownItems = [ + + {t('Rerun')} + , + ...(!shouldHidePipelineRunCancel(obj, PLRTasks) + ? [ + + {t('Cancel')} + , + ] + : []), + ...(!shouldHidePipelineRunStop(obj, PLRTasks) + ? [ + + {t('Stop')} + , + ] + : []), + + {t('Delete PipelineRun')} + , + ]; + + return ( + setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + + + + )} + isOpen={isOpen} + isPlain={false} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ); +}; + +export default PipelineRunsKebab; diff --git a/src/components/pipelineRuns-list/PipelineRunsList.scss b/src/components/pipelineRuns-list/PipelineRunsList.scss new file mode 100644 index 00000000..ced63edc --- /dev/null +++ b/src/components/pipelineRuns-list/PipelineRunsList.scss @@ -0,0 +1,17 @@ +.opp-pipeline-run-list { + &__signed-indicator { + display: inline-block; + --pf-c-table--cell--Color: var( + --pf-global--BackgroundColor--dark-transparent-100 + ); + margin-left: var(--pf-global--spacer--xs); + vertical-align: middle; + } + &__results-indicator { + display: inline-block; + margin-left: var(--pf-global--spacer--xs); + > svg { + color: var(--pf-global--Color--400); + } + } +} diff --git a/src/components/pipelineRuns-list/PipelineRunsList.tsx b/src/components/pipelineRuns-list/PipelineRunsList.tsx new file mode 100644 index 00000000..621de826 --- /dev/null +++ b/src/components/pipelineRuns-list/PipelineRunsList.tsx @@ -0,0 +1,80 @@ +import { + ListPageBody, + ListPageFilter, + VirtualizedTable, + useListPageFilter, +} from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import usePipelineRunsColumns from './usePipelineRunsColumns'; +import { usePipelineRunsFilters } from './usePipelineRunsFilters'; +import { PipelineRunKind } from '../../types'; +import { useGetPipelineRuns, useGetTaskRuns } from '../hooks/useTektonResult'; +import PipelineRunsRow from './PipelineRunsRow'; +import { useTranslation } from 'react-i18next'; + +import './PipelineRunsList.scss'; +import { useParams } from 'react-router-dom-v5-compat'; + +type PipelineRunsListProps = { + namespace?: string; + hideTextFilter?: boolean; +}; + +const PipelineRunsList: React.FC = ({ + namespace, + hideTextFilter, +}) => { + const { t } = useTranslation(); + const { ns } = useParams(); + namespace = namespace || ns; + const columns = usePipelineRunsColumns(namespace); + const filters = usePipelineRunsFilters(); + + const [pipelineRuns, pipelineRunsLoaded, pipelineRunsLoadError] = + useGetPipelineRuns(namespace); + const [data, filteredData, onFilterChange] = useListPageFilter( + pipelineRuns, + filters, + ); + const [taskRuns, taskRunsLoaded] = useGetTaskRuns(namespace); + return ( + + ({ id, title })), + id: 'pipelineRuns-list', + type: 'PipelineRun', + selectedColumns: new Set(['name']), + }} + rowFilters={filters} + onFilterChange={onFilterChange} + data={data} + loaded={pipelineRunsLoaded} + hideColumnManagement + hideNameLabelFilters={hideTextFilter} + /> + + EmptyMsg={() => ( +
+ {t('No PipelineRuns found')} +
+ )} + columns={columns} + data={filteredData} + loaded={pipelineRunsLoaded} + loadError={pipelineRunsLoadError} + Row={PipelineRunsRow} + rowData={{ + taskRuns, + taskRunsLoaded, + }} + unfilteredData={data} + /> +
+ ); +}; + +export default PipelineRunsList; diff --git a/src/components/pipelineRuns-list/PipelineRunsListPage.tsx b/src/components/pipelineRuns-list/PipelineRunsListPage.tsx new file mode 100644 index 00000000..562ba6f2 --- /dev/null +++ b/src/components/pipelineRuns-list/PipelineRunsListPage.tsx @@ -0,0 +1,43 @@ +import { ListPageHeader } from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { PipelineRunModel } from '../../models'; +import PipelineRunsList from './PipelineRunsList'; +import ListPageCreateButton from '../list-pages/ListPageCreateButton'; + +type PipelineRunsListPageProps = { + namespace: string; + hideTextFilter?: boolean; +}; + +const PipelineRunsListPage: React.FC = (props) => { + const { t } = useTranslation(); + const { namespace, hideTextFilter } = props; + return ( + <> + {hideTextFilter ? ( + <> + + + + ) : ( + <> + + + + + + )} + + ); +}; + +export default PipelineRunsListPage; diff --git a/src/components/pipelineRuns-list/PipelineRunsRow.tsx b/src/components/pipelineRuns-list/PipelineRunsRow.tsx new file mode 100644 index 00000000..04ecdb98 --- /dev/null +++ b/src/components/pipelineRuns-list/PipelineRunsRow.tsx @@ -0,0 +1,173 @@ +import { + ResourceLink, + RowProps, + TableData, + Timestamp, + getGroupVersionKindForModel, +} from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import { ArchiveIcon } from '@patternfly/react-icons'; +import { PipelineRunKind, TaskRunKind } from '../../types'; +import { ResourceLinkWithIcon } from '../utils/resource-link'; +import { PipelineRunModel } from '../../models'; +import { Tooltip } from '@patternfly/react-core'; +import { + DELETED_RESOURCE_IN_K8S_ANNOTATION, + RESOURCE_LOADED_FROM_RESULTS_ANNOTATION, + chainsSignedAnnotation, +} from '../../consts'; +import { useTranslation } from 'react-i18next'; +import SignedBadgeIcon from '../../images/SignedBadge'; +import PipelineRunVulnerabilities from '../pipelines-list/status/PipelineRunVulnerabilities'; +import PipelineRunStatus from '../pipelines-list/status/PipelineRunStatus'; +import { getTaskRunsOfPipelineRun } from '../hooks/useTaskRuns'; +import { + pipelineRunFilterReducer, + pipelineRunTitleFilterReducer, +} from '../utils/pipeline-filter-reducer'; +import LinkedPipelineRunTaskStatus from '../pipelines-list/status/LinkedPipelineRunTaskStatus'; +import { pipelineRunDuration } from '../utils/pipelines-utils'; +import PipelineRunsKebab from './PipelineRunsKebab'; + +export const tableColumnClasses = { + name: 'pf-m-width-20', + namespace: '', + vulnerabilities: 'pf-m-hidden pf-m-visible-on-md', + status: 'pf-m-hidden pf-m-visible-on-sm pf-m-width-10', + taskStatus: 'pf-m-hidden pf-m-visible-on-lg', + started: 'pf-m-hidden pf-m-visible-on-lg', + duration: 'pf-m-hidden pf-m-visible-on-xl', + actions: 'dropdown-kebab-pf pf-v5-c-table__action', +}; + +type PLRStatusProps = { + obj: PipelineRunKind; + taskRuns: TaskRunKind[]; + taskRunsLoaded: boolean; +}; + +const PLRStatus: React.FC = ({ + obj, + taskRuns, + taskRunsLoaded, +}) => { + return ( + + ); +}; + +const PipelineRunsRow: React.FC< + RowProps< + PipelineRunKind, + { taskRuns: TaskRunKind[]; taskRunsLoaded: boolean } + > +> = ({ obj, activeColumnIDs, rowData: { taskRuns, taskRunsLoaded } }) => { + const { t } = useTranslation(); + const PLRTaskRuns = getTaskRunsOfPipelineRun(taskRuns, obj?.metadata?.name); + return ( + <> + + + {obj?.metadata?.annotations?.[chainsSignedAnnotation] === + 'true' ? ( + +
+ +
+
+ ) : null} + {obj?.metadata?.annotations?.[ + DELETED_RESOURCE_IN_K8S_ANNOTATION + ] === 'true' || + obj?.metadata?.annotations?.[ + RESOURCE_LOADED_FROM_RESULTS_ANNOTATION + ] === 'true' ? ( + +
+ +
+
+ ) : null} + + } + /> +
+ + + + + + + + + + + + + + + + + {pipelineRunDuration(obj)} + + + + + + ); +}; + +export default PipelineRunsRow; diff --git a/src/components/pipelineRuns-list/index.ts b/src/components/pipelineRuns-list/index.ts new file mode 100644 index 00000000..eb91173d --- /dev/null +++ b/src/components/pipelineRuns-list/index.ts @@ -0,0 +1,2 @@ +export { default as PipelineRunsList } from './PipelineRunsList'; +export { default as PipelineRunsListPage } from './PipelineRunsListPage'; diff --git a/src/components/pipelineRuns-list/usePipelineRunsColumns.ts b/src/components/pipelineRuns-list/usePipelineRunsColumns.ts new file mode 100644 index 00000000..4a8f9f4b --- /dev/null +++ b/src/components/pipelineRuns-list/usePipelineRunsColumns.ts @@ -0,0 +1,72 @@ +import { TableColumn } from '@openshift-console/dynamic-plugin-sdk'; +import { sortable } from '@patternfly/react-table'; +import { useTranslation } from 'react-i18next'; +import { PipelineRunKind } from '../../types'; +import { tableColumnClasses } from './PipelineRunsRow'; + +const usePipelineRunsColumns = (namespace): TableColumn[] => { + const { t } = useTranslation(); + const columns = [ + { + id: 'name', + title: t('Name'), + sort: 'metadata.name', + transforms: [sortable], + props: { className: tableColumnClasses.name }, + }, + ...(!namespace + ? [ + { + id: 'namespace', + title: t('Namespace'), + sort: 'metadata.namespace', + transforms: [sortable], + props: { className: tableColumnClasses.namespace }, + }, + ] + : []), + { + id: 'vulnerabilities', + title: t('Vulnerabilities'), + sortFunc: 'vulnerabilities', + transforms: [sortable], + props: { className: tableColumnClasses.vulnerabilities }, + }, + { + id: 'status', + title: t('Status'), + sort: 'status.conditions[0].reason', + transforms: [sortable], + props: { className: tableColumnClasses.status }, + }, + { + id: 'task-status', + title: t('Task status'), + sort: 'status.conditions[0].reason', + transforms: [sortable], + props: { className: tableColumnClasses.taskStatus }, + }, + { + id: 'started', + title: t('Started'), + sort: 'status.startTime', + transforms: [sortable], + props: { className: tableColumnClasses.started }, + }, + { + id: 'duration', + title: t('Duration'), + sort: 'status.completionTime', + transforms: [sortable], + props: { className: tableColumnClasses.duration }, + }, + { + id: 'kebab-menu', + title: '', + props: { className: tableColumnClasses.actions }, + }, + ]; + return columns; +}; + +export default usePipelineRunsColumns; diff --git a/src/components/pipelineRuns-list/usePipelineRunsFilters.ts b/src/components/pipelineRuns-list/usePipelineRunsFilters.ts new file mode 100644 index 00000000..8c2f189e --- /dev/null +++ b/src/components/pipelineRuns-list/usePipelineRunsFilters.ts @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import { + pipelineRunFilterReducer, + pipelineRunStatusFilter, +} from '../utils/pipeline-filter-reducer'; +import { ListFilterId, ListFilterLabels } from '../utils/pipeline-utils'; + +export const usePipelineRunsFilters = () => { + const { t } = useTranslation(); + return [ + { + filterGroupName: t('Status'), + type: 'pipeline-status', + reducer: pipelineRunFilterReducer, + items: [ + { + id: ListFilterId.Succeeded, + title: ListFilterLabels[ListFilterId.Succeeded], + }, + { + id: ListFilterId.Running, + title: ListFilterLabels[ListFilterId.Running], + }, + { + id: ListFilterId.Failed, + title: ListFilterLabels[ListFilterId.Failed], + }, + { + id: ListFilterId.Cancelled, + title: ListFilterLabels[ListFilterId.Cancelled], + }, + { id: ListFilterId.Other, title: ListFilterLabels[ListFilterId.Other] }, + ], + filter: pipelineRunStatusFilter, + }, + ]; +}; diff --git a/src/components/pipelines-list/PipelineListPage.tsx b/src/components/pipelines-list/PipelineListPage.tsx index f5d46a2b..34e8b3c4 100644 --- a/src/components/pipelines-list/PipelineListPage.tsx +++ b/src/components/pipelines-list/PipelineListPage.tsx @@ -1,35 +1,39 @@ -import { - ListPageCreateLink, - ListPageHeader, -} from '@openshift-console/dynamic-plugin-sdk'; +import { ListPageHeader } from '@openshift-console/dynamic-plugin-sdk'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { PipelineModel } from '../../models'; import PipelinesList from './PipelinesList'; -import { getReferenceForModel } from '../pipelines-overview/utils'; +import ListPageCreateButton from '../list-pages/ListPageCreateButton'; type PipelineListPageProps = { - namespace: string; + namespace?: string; + hideTextFilter?: boolean; }; -const PipelineListPage: React.FC = ({ namespace }) => { +const PipelineListPage: React.FC = (props) => { const { t } = useTranslation('plugin__pipelines-console-plugin'); + const { namespace, hideTextFilter } = props; return ( <> - - - {t('Create Pipeline')} - - - + {hideTextFilter ? ( + <> + + + + ) : ( + + + + + )} ); }; diff --git a/src/components/pipelines-list/PipelineRow.tsx b/src/components/pipelines-list/PipelineRow.tsx index 6d4cc9d4..9b6707fd 100644 --- a/src/components/pipelines-list/PipelineRow.tsx +++ b/src/components/pipelines-list/PipelineRow.tsx @@ -29,22 +29,31 @@ export const tableColumnClasses = [ type PipelineStatusProps = { obj: PipelineWithLatest; taskRuns: TaskRunKind[]; + taskRunsLoaded: boolean; }; -const PipelineStatus: React.FC = ({ obj, taskRuns }) => { +const PipelineStatus: React.FC = ({ + obj, + taskRuns, + taskRunsLoaded, +}) => { return ( ); }; const PipelineRow: React.FC< - RowProps -> = ({ obj, activeColumnIDs, rowData: { taskRuns } }) => { + RowProps< + PipelineWithLatest, + { taskRuns: TaskRunKind[]; taskRunsLoaded: boolean } + > +> = ({ obj, activeColumnIDs, rowData: { taskRuns, taskRunsLoaded } }) => { const PLRTaskRuns = getTaskRunsOfPipelineRun( taskRuns, obj?.latestRun?.metadata?.name, @@ -93,6 +102,7 @@ const PipelineRow: React.FC< ) : ( '-' @@ -103,7 +113,11 @@ const PipelineRow: React.FC< id="last-run-status" activeColumnIDs={activeColumnIDs} > - + = ({ namespace }) => { +const PipelinesList: React.FC = ({ + namespace, + hideTextFilter, +}) => { + const { t } = useTranslation(); + const { ns } = useParams(); + namespace = namespace || ns; const columns = usePipelinesColumns(namespace); const filters = usePipelinesFilters(); const [pipelines, pipelinesLoaded, pipelinesLoadError] = useK8sWatchResource< @@ -43,7 +52,7 @@ const PipelinesList: React.FC = ({ namespace }) => { pipelinesData, filters, ); - const [taskRuns] = useGetTaskRuns(namespace); + const [taskRuns, taskRunsLoaded] = useGetTaskRuns(namespace); return ( = ({ namespace }) => { data={data} loaded={pipelinesLoaded && pipelineRunsLoaded} hideColumnManagement + hideNameLabelFilters={hideTextFilter} /> EmptyMsg={() => ( -
- No Pipelines found +
+ {t('No Pipelines found')}
)} columns={columns} @@ -72,6 +85,7 @@ const PipelinesList: React.FC = ({ namespace }) => { Row={PipelineRow} rowData={{ taskRuns, + taskRunsLoaded, }} unfilteredData={data} /> diff --git a/src/components/pipelines-list/PipelinesTabbedPage.tsx b/src/components/pipelines-list/PipelinesTabbedPage.tsx new file mode 100644 index 00000000..430d000c --- /dev/null +++ b/src/components/pipelines-list/PipelinesTabbedPage.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + NamespaceBar, + NavPage, + useFlag, +} from '@openshift-console/dynamic-plugin-sdk'; +import { PipelineModel, PipelineRunModel, RepositoryModel } from '../../models'; +import { RepositoriesList } from '../repositories-list'; +import { PipelinesList } from '../pipelines-list'; +import { PipelineRunsList } from '../pipelineRuns-list'; +import { FLAG_OPENSHIFT_PIPELINE_AS_CODE } from '../../consts'; +import { + MenuAction, + MenuActions, +} from '../multi-tab-list/multi-tab-list-page-types'; +import { MultiTabListPage } from '../multi-tab-list'; + +export const PageContents: React.FC = () => { + const { t } = useTranslation(); + const isRepositoryEnabled = useFlag(FLAG_OPENSHIFT_PIPELINE_AS_CODE); + + const menuActions: MenuActions = { + pipeline: { + model: PipelineModel, + onSelection: (key: string, action: MenuAction, url: string) => + `${url}/builder`, + }, + pipelineRun: { model: PipelineRunModel }, + repository: { + model: RepositoryModel, + onSelection: (_key: string, _action: MenuAction, url: string) => + `${url}/form`, + }, + }; + const pages: NavPage[] = [ + { + href: '', + // t(PipelineModel.labelPluralKey) + name: PipelineModel.labelPluralKey, + component: PipelinesList, + }, + { + href: 'pipeline-runs', + // t(PipelineRunModel.labelPluralKey) + name: PipelineRunModel.labelPluralKey, + component: PipelineRunsList, + }, + ...(isRepositoryEnabled + ? [ + { + href: 'repositories', + // t(RepositoryModel.labelPluralKey) + name: RepositoryModel.labelPluralKey, + component: RepositoriesList, + }, + ] + : []), + ]; + + // return namespace ? ( + // + // ) : ( + // + // {(openProjectModal) => ( + // + // Select a Project to view its details + // . + // + // )} + // + // ); + return ( + + ); +}; + +// const PageContentsWithStartGuide = withStartGuide(PageContents); + +const PipelinesTabbedPage: React.FC = (props) => { + return ( + <> + + + + ); +}; + +export default PipelinesTabbedPage; diff --git a/src/components/pipelines-list/index.ts b/src/components/pipelines-list/index.ts index 51d0245c..2d874902 100644 --- a/src/components/pipelines-list/index.ts +++ b/src/components/pipelines-list/index.ts @@ -1,2 +1,3 @@ export { default as PipelineListPage } from './PipelineListPage'; export { default as PipelinesList } from './PipelinesList'; +export { default as PipelinesTabbedPage } from './PipelinesTabbedPage'; diff --git a/src/components/pipelines-list/status/LinkedPipelineRunTaskStatus.tsx b/src/components/pipelines-list/status/LinkedPipelineRunTaskStatus.tsx index a7e9cfcf..969e35b5 100644 --- a/src/components/pipelines-list/status/LinkedPipelineRunTaskStatus.tsx +++ b/src/components/pipelines-list/status/LinkedPipelineRunTaskStatus.tsx @@ -10,6 +10,7 @@ import { getReferenceForModel } from '../../pipelines-overview/utils'; export interface LinkedPipelineRunTaskStatusProps { pipelineRun: PipelineRunKind; taskRuns: TaskRunKind[]; + taskRunsLoaded: boolean; } /** @@ -18,8 +19,11 @@ export interface LinkedPipelineRunTaskStatusProps { */ const LinkedPipelineRunTaskStatus: React.FC< LinkedPipelineRunTaskStatusProps -> = ({ pipelineRun, taskRuns }) => { +> = ({ pipelineRun, taskRuns, taskRunsLoaded }) => { const { t } = useTranslation('plugin__pipelines-console-plugin'); + if (taskRunsLoaded && taskRuns.length === 0) { + return <>{'-'}; + } const pipelineStatus = taskRuns.length > 0 ? ( = ({ status, pipelineRun, title, taskRuns, + taskRunsLoaded, }) => { const { t } = useTranslation('plugin__pipelines-console-plugin'); const logPath = `/k8s/ns/${ - pipelineRun.metadata.namespace + pipelineRun?.metadata.namespace }/${getReferenceForModel(PipelineRunModel)}/${ - pipelineRun.metadata.name + pipelineRun?.metadata.name }/logs`; + if (taskRunsLoaded && taskRuns.length === 0) { + return <>{'-'}; + } return pipelineRun ? ( taskRuns.length > 0 ? ( {t('View logs')}} /> diff --git a/src/components/pipelines-tasks/TaskRunsRow.tsx b/src/components/pipelines-tasks/TaskRunsRow.tsx index 767801a6..52048cc1 100644 --- a/src/components/pipelines-tasks/TaskRunsRow.tsx +++ b/src/components/pipelines-tasks/TaskRunsRow.tsx @@ -38,6 +38,10 @@ import TaskRunStatus from './TaskRunStatus'; import { ResourceLinkWithIcon } from '../utils/resource-link'; import './TasksNavigationPage.scss'; +import { + isResourceLoadedFromTR, + tektonResultsFlag, +} from '../utils/common-utils'; const taskRunsReference = getReferenceForModel(TaskRunModel); const pipelineReference = getReferenceForModel(PipelineModel); @@ -58,15 +62,8 @@ const TaskRunKebab: React.FC = ({ obj }) => {

); - const tektonResultsFlag = - obj?.metadata?.annotations?.['results.tekton.dev/log'] || - obj?.metadata?.annotations?.['results.tekton.dev/record'] || - obj?.metadata?.annotations?.['results.tekton.dev/result']; - const isResourceLoadedFromTR = - obj?.metadata?.annotations?.[RESOURCE_LOADED_FROM_RESULTS_ANNOTATION]; - const launchDeleteModal = - !isResourceLoadedFromTR && tektonResultsFlag + !isResourceLoadedFromTR(obj) && tektonResultsFlag(obj) ? useDeleteModal(obj, undefined, message) : useDeleteModal(obj); const { name, namespace } = obj.metadata; diff --git a/src/components/repositories-list/RepositoriesKebab.tsx b/src/components/repositories-list/RepositoriesKebab.tsx new file mode 100644 index 00000000..3cf0db84 --- /dev/null +++ b/src/components/repositories-list/RepositoriesKebab.tsx @@ -0,0 +1,57 @@ +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { K8sCommonKebabMenu } from '../utils/k8s-common-kebab-menu'; +import * as React from 'react'; +import { RepositoryModel } from '../../models'; +import { + Dropdown, + DropdownList, + MenuToggle, + MenuToggleElement, +} from '@patternfly/react-core'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import { KEBAB_BUTTON_ID } from '../../consts'; + +type RepositoriesKebabProps = { + obj: K8sResourceCommon; +}; + +const RepositoriesKebab: React.FC = ({ obj }) => { + const [isOpen, setIsOpen] = React.useState(false); + + const onToggle = () => { + setIsOpen(!isOpen); + }; + + const onSelect = () => { + setIsOpen(false); + }; + + const dropdownItems = K8sCommonKebabMenu(obj, RepositoryModel); + + return ( + setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + + + + )} + isOpen={isOpen} + isPlain={false} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ); +}; + +export default RepositoriesKebab; diff --git a/src/components/repositories-list/RepositoriesList.tsx b/src/components/repositories-list/RepositoriesList.tsx new file mode 100644 index 00000000..bd0eca68 --- /dev/null +++ b/src/components/repositories-list/RepositoriesList.tsx @@ -0,0 +1,83 @@ +import { + ListPageBody, + ListPageFilter, + VirtualizedTable, + getGroupVersionKindForModel, + useK8sWatchResource, + useListPageFilter, +} from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import { PipelineRunModel, RepositoryModel } from '../../models'; +import { PipelineRunKind, RepositoryKind } from '../../types'; +import useRepositoriesColumns from './useRepositoriesColumns'; +import RepositoriesRow from './RepositoriesRow'; +import { useGetTaskRuns } from '../hooks/useTektonResult'; +import { useParams } from 'react-router-dom-v5-compat'; +import { useTranslation } from 'react-i18next'; + +type RepositoriesListProps = { + namespace?: string; + hideTextFilter?: boolean; +}; + +const RepositoriesList: React.FC = ({ + namespace, + hideTextFilter, +}) => { + const { t } = useTranslation(); + const { ns } = useParams(); + namespace = namespace || ns; + const columns = useRepositoriesColumns(namespace); + const [repositories, repositoriesLoaded, repositoriesLoadError] = + useK8sWatchResource({ + groupVersionKind: getGroupVersionKindForModel(RepositoryModel), + isList: true, + namespace, + optional: true, + }); + const [pipelineRuns, pipelineRunsLoaded] = useK8sWatchResource< + PipelineRunKind[] + >({ + isList: true, + groupVersionKind: getGroupVersionKindForModel(PipelineRunModel), + namespace, + optional: true, + }); + const [taskRuns, taskRunsLoaded] = useGetTaskRuns(namespace); + const [staticData, filteredData, onFilterChange] = + useListPageFilter(repositories); + + return ( + + + ( +
+ {t('No Repositories found')} +
+ )} + columns={columns} + data={filteredData} + loaded={repositoriesLoaded && pipelineRunsLoaded} + loadError={repositoriesLoadError} + Row={RepositoriesRow} + rowData={{ + taskRuns, + pipelineRuns, + taskRunsLoaded, + }} + unfilteredData={staticData} + /> +
+ ); +}; + +export default RepositoriesList; diff --git a/src/components/repositories-list/RepositoriesListPage.tsx b/src/components/repositories-list/RepositoriesListPage.tsx new file mode 100644 index 00000000..f0321179 --- /dev/null +++ b/src/components/repositories-list/RepositoriesListPage.tsx @@ -0,0 +1,41 @@ +import { ListPageHeader } from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { RepositoryModel } from '../../models'; +import RepositoriesList from './RepositoriesList'; +import ListPageCreateButton from '../list-pages/ListPageCreateButton'; + +type RepositoriesListPageProps = { + namespace?: string; + hideTextFilter?: boolean; +}; + +const RepositoriesListPage: React.FC = (props) => { + const { t } = useTranslation(); + const { namespace, hideTextFilter } = props; + return ( + <> + {hideTextFilter ? ( + <> + + + + ) : ( + + + + + )} + + ); +}; + +export default RepositoriesListPage; diff --git a/src/components/repositories-list/RepositoriesRow.tsx b/src/components/repositories-list/RepositoriesRow.tsx new file mode 100644 index 00000000..e749ebe5 --- /dev/null +++ b/src/components/repositories-list/RepositoriesRow.tsx @@ -0,0 +1,176 @@ +import { + ResourceIcon, + ResourceLink, + RowProps, + TableData, + Timestamp, + getGroupVersionKindForModel, +} from '@openshift-console/dynamic-plugin-sdk'; +import * as React from 'react'; +import { PipelineRunKind, RepositoryKind, TaskRunKind } from '../../types'; +import { Link } from 'react-router-dom'; +import { PipelineRunModel, RepositoryModel } from '../../models'; +import { getLatestRun } from '../utils/pipeline-augment'; +import { getTaskRunsOfPipelineRun } from '../hooks/useTaskRuns'; +import { RepositoryFields, RepositoryLabels } from '../../consts'; +import { pipelineRunDuration } from '../utils/pipeline-utils'; +import PipelineRunStatus from '../pipelines-list/status/PipelineRunStatus'; +import { + pipelineRunFilterReducer, + pipelineRunTitleFilterReducer, +} from '../utils/pipeline-filter-reducer'; +import { getReferenceForModel } from '../pipelines-overview/utils'; +import LinkedPipelineRunTaskStatus from '../pipelines-list/status/LinkedPipelineRunTaskStatus'; +import RepositoriesKebab from './RepositoriesKebab'; + +export const repositoriesTableColumnClasses = [ + 'pf-v5-u-w-16-on-xl pf-v5-u-w-25-on-lg pf-v5-u-w-33-on-xs', // name + 'pf-v5-u-w-12-on-xl pf-v5-u-w-20-on-lg pf-v5-u-w-30-on-xs', // namespace + 'pf-v5-u-w-12-on-xl pf-v5-u-w-20-on-lg pf-v5-u-w-30-on-xs', // Event type + 'pf-v5-u-w-12-on-xl pf-v5-u-w-20-on-lg pf-v5-u-w-30-on-xs', // Last run + 'pf-v5-u-w-16-on-xl pf-v5-u-w-25-on-lg pf-v5-u-w-33-on-xs', // Task status + 'pf-m-hidden pf-m-visible-on-xl', // last run status + 'pf-m-hidden pf-v5-u-w-12-on-xl pf-v5-u-w-20-on-lg pf-v5-u-w-33-on-xs pf-m-visible-on-xl', // Last run time + 'pf-m-hidden pf-m-visible-on-xl', // Last run duration + 'dropdown-kebab-pf pf-v5-c-table__action', +]; + +const RepositoriesRow: React.FC< + RowProps< + RepositoryKind, + { + taskRuns: TaskRunKind[]; + pipelineRuns: PipelineRunKind[]; + taskRunsLoaded: boolean; + } + > +> = ({ + obj, + activeColumnIDs, + rowData: { taskRuns, pipelineRuns, taskRunsLoaded }, +}) => { + const { + metadata: { name, namespace }, + } = obj; + const plrs = pipelineRuns.filter((plr) => { + return ( + plr.metadata?.labels?.[RepositoryLabels[RepositoryFields.REPOSITORY]] === + obj.metadata.name + ); + }); + const latestRun = getLatestRun(plrs, 'creationTimestamp'); + + const latestPLREventType = + latestRun && + latestRun?.metadata?.labels[RepositoryLabels[RepositoryFields.EVENT_TYPE]]; + + const PLRTaskRuns = getTaskRunsOfPipelineRun( + taskRuns, + latestRun?.metadata?.name, + ); + return ( + <> + + + + {name} + + + + + + + {latestPLREventType || '-'} + + + {latestRun ? ( + + ) : ( + '-' + )} + + + {} + {latestRun ? ( + + ) : ( + '-' + )} + + + { + + } + + + {} + + + {pipelineRunDuration(latestRun)} + + + + + + ); +}; + +export default RepositoriesRow; diff --git a/src/components/repositories-list/index.ts b/src/components/repositories-list/index.ts new file mode 100644 index 00000000..db255ef7 --- /dev/null +++ b/src/components/repositories-list/index.ts @@ -0,0 +1,2 @@ +export { default as RepositoriesList } from './RepositoriesList'; +export { default as RepositoriesListPage } from './RepositoriesListPage'; diff --git a/src/components/repositories-list/useRepositoriesColumns.ts b/src/components/repositories-list/useRepositoriesColumns.ts new file mode 100644 index 00000000..029d4fe3 --- /dev/null +++ b/src/components/repositories-list/useRepositoriesColumns.ts @@ -0,0 +1,73 @@ +import { TableColumn } from '@openshift-console/dynamic-plugin-sdk'; +import { sortable } from '@patternfly/react-table'; +import { useTranslation } from 'react-i18next'; +import { RepositoryKind } from '../../types'; +import { repositoriesTableColumnClasses } from './RepositoriesRow'; + +const useRepositoriesColumns = (namespace): TableColumn[] => { + const { t } = useTranslation(); + return [ + { + id: 'name', + title: t('Name'), + sort: 'metadata.name', + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[0] }, + }, + ...(!namespace + ? [ + { + title: t('Namespace'), + sort: 'metadata.namespace', + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[1] }, + id: 'namespace', + }, + ] + : []), + { + id: 'event-type', + title: t('Event type'), + sort: 'spec.event_type', + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[2] }, + }, + { + id: 'last-run', + title: t('Last run'), + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[3] }, + }, + { + id: 'task-status', + title: t('Task status'), + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[4] }, + }, + { + id: 'last-run-status', + title: t('Last run status'), + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[5] }, + }, + { + id: 'last-runtime', + title: t('Last run time'), + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[6] }, + }, + { + id: 'last-run-duration', + title: t('Last run duration'), + transforms: [sortable], + props: { className: repositoriesTableColumnClasses[7] }, + }, + { + id: 'kebab-menu', + title: '', + props: { className: repositoriesTableColumnClasses[8] }, + }, + ]; +}; + +export default useRepositoriesColumns; diff --git a/src/components/utils/common-utils.ts b/src/components/utils/common-utils.ts index 4bc87302..46062145 100644 --- a/src/components/utils/common-utils.ts +++ b/src/components/utils/common-utils.ts @@ -4,6 +4,7 @@ import { TOptions } from 'i18next'; import _ from 'lodash'; import React from 'react'; import { getI18n } from 'react-i18next'; +import { RESOURCE_LOADED_FROM_RESULTS_ANNOTATION } from '../../consts'; import { Options, resourceURL } from './k8s-utils'; export const t = (value: string, options?: TOptions) => @@ -33,3 +34,11 @@ export const watchURL = (kind: K8sKind, options: Options): string => { opts.queryParams.watch = 'true'; return resourceURL(kind, opts); }; + +export const tektonResultsFlag = (obj) => + obj?.metadata?.annotations?.['results.tekton.dev/log'] || + obj?.metadata?.annotations?.['results.tekton.dev/record'] || + obj?.metadata?.annotations?.['results.tekton.dev/result']; + +export const isResourceLoadedFromTR = (obj) => + obj?.metadata?.annotations?.[RESOURCE_LOADED_FROM_RESULTS_ANNOTATION]; diff --git a/src/components/utils/utils.ts b/src/components/utils/utils.ts index 69716c6a..272a0428 100644 --- a/src/components/utils/utils.ts +++ b/src/components/utils/utils.ts @@ -1,9 +1,21 @@ import { - K8sModel, getGroupVersionKindForModel, + K8sModel, } from '@openshift-console/dynamic-plugin-sdk'; import _ from 'lodash'; -import { RouteIngress, RouteKind } from 'src/types'; +import { + DELETED_RESOURCE_IN_K8S_ANNOTATION, + preferredNameAnnotation, + RESOURCE_LOADED_FROM_RESULTS_ANNOTATION, +} from '../../consts'; +import { PipelineRunModel } from '../../models'; +import { + PipelineKind, + PipelineRunKind, + RouteIngress, + RouteKind, +} from '../../types'; +import { getPipelineRunParams } from './pipeline-utils'; export const resourcePathFromModel = ( model: K8sModel, @@ -70,3 +82,149 @@ export const getRouteWebURL = (route: RouteKind): string => { } return url; }; + +export const getPipelineName = ( + pipeline?: PipelineKind, + latestRun?: PipelineRunKind, +): string => { + if (pipeline) { + return pipeline.metadata.name; + } + + if (latestRun) { + return ( + latestRun.spec.pipelineRef?.name ?? + (latestRun.metadata.annotations?.[preferredNameAnnotation] || + latestRun.metadata.name) + ); + } + return null; +}; + +export const getPipelineRunGenerateName = ( + pipelineRun: PipelineRunKind, +): string => { + if (pipelineRun.metadata.generateName) { + return pipelineRun.metadata.generateName; + } + + return `${pipelineRun.metadata.name?.replace(/-[a-z0-9]{5,6}$/, '')}-`; +}; + +export const getRandomChars = (len = 6): string => { + return Math.random() + .toString(36) + .replace(/[^a-z0-9]+/g, '') + .substr(1, len); +}; + +/** + * Migrates a PipelineRun from one version to another to support auto-upgrades with old (and invalid) PipelineRuns. + * + * Note: Each check within this method should be driven by the apiVersion number if the API is properly up-versioned + * for these breaking changes. (should be done moving from 0.10.x forward) + */ +export const migratePipelineRun = ( + pipelineRun: PipelineRunKind, +): PipelineRunKind => { + let newPipelineRun = pipelineRun; + + const serviceAccountPath = 'spec.serviceAccount'; + if (_.has(newPipelineRun, serviceAccountPath)) { + // .spec.serviceAccount was removed for .spec.serviceAccountName in 0.9.x + // Note: apiVersion was not updated for this change and thus we cannot gate this change behind a version number + const serviceAccountName = _.get(newPipelineRun, serviceAccountPath); + newPipelineRun = _.omit(newPipelineRun, [ + serviceAccountPath, + ]) as PipelineRunKind; + newPipelineRun = _.merge(newPipelineRun, { + spec: { + serviceAccountName, + }, + }); + } + + return newPipelineRun; +}; + +export const getPipelineRunData = ( + pipeline: PipelineKind = null, + latestRun?: PipelineRunKind, + options?: { generateName: boolean }, +): PipelineRunKind => { + if (!pipeline && !latestRun) { + // eslint-disable-next-line no-console + console.error('Missing parameters, unable to create new PipelineRun'); + return null; + } + + const pipelineName = getPipelineName(pipeline, latestRun); + + const workspaces = latestRun?.spec.workspaces; + + const latestRunParams = latestRun?.spec.params; + const pipelineParams = pipeline?.spec.params; + const params = latestRunParams || getPipelineRunParams(pipelineParams); + // TODO: We should craft a better method to allow us to provide configurable annotations and labels instead of + // blinding merging existing content from potential real Pipeline and PipelineRun resources + const annotations = _.merge( + {}, + pipeline?.metadata?.annotations, + latestRun?.metadata?.annotations, + // { + // [StartedByAnnotation.user]: getActiveUserName(), + // }, + !latestRun?.spec.pipelineRef && + !latestRun?.metadata.annotations?.[preferredNameAnnotation] && { + [preferredNameAnnotation]: pipelineName, + }, + ); + delete annotations['kubectl.kubernetes.io/last-applied-configuration']; + delete annotations['tekton.dev/v1beta1TaskRuns']; + delete annotations['results.tekton.dev/log']; + delete annotations['results.tekton.dev/record']; + delete annotations['results.tekton.dev/result']; + delete annotations[DELETED_RESOURCE_IN_K8S_ANNOTATION]; + delete annotations[RESOURCE_LOADED_FROM_RESULTS_ANNOTATION]; + + const newPipelineRun = { + apiVersion: pipeline ? pipeline.apiVersion : latestRun.apiVersion, + kind: PipelineRunModel.kind, + metadata: { + ...(options?.generateName + ? { + generateName: `${pipelineName}-`, + } + : { + name: + latestRun?.metadata?.name !== undefined + ? `${getPipelineRunGenerateName(latestRun)}${getRandomChars()}` + : `${pipelineName}-${getRandomChars()}`, + }), + annotations, + namespace: pipeline + ? pipeline.metadata.namespace + : latestRun.metadata.namespace, + labels: _.merge( + {}, + pipeline?.metadata?.labels, + latestRun?.metadata?.labels, + (latestRun?.spec.pipelineRef || pipeline) && { + 'tekton.dev/pipeline': pipelineName, + }, + ), + }, + spec: { + ...(latestRun?.spec || {}), + ...((latestRun?.spec.pipelineRef || pipeline) && { + pipelineRef: { + name: pipelineName, + }, + }), + ...(params && { params }), + workspaces, + status: null, + }, + }; + return migratePipelineRun(newPipelineRun); +}; diff --git a/src/consts.ts b/src/consts.ts index 5bcd0645..5ac27146 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -116,3 +116,7 @@ export const KEBAB_ACTION_EDIT_LABELS_ID = 'edit-labels'; export const KEBAB_BUTTON_ID = 'kebab-button'; export const DELETED_RESOURCE_IN_K8S_ANNOTATION = 'resource.deleted.in.k8s'; + +export const chainsSignedAnnotation = 'chains.tekton.dev/signed'; +export const preferredNameAnnotation = 'pipeline.openshift.io/preferredName'; +export const FLAG_OPENSHIFT_PIPELINE_AS_CODE = 'OPENSHIFT_PIPELINE_AS_CODE'; diff --git a/src/images/SignedBadge.tsx b/src/images/SignedBadge.tsx new file mode 100644 index 00000000..e2f5a6d4 --- /dev/null +++ b/src/images/SignedBadge.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +const SignedBadgeIcon: React.FC> = ( + props, +): React.ReactElement => { + return ( + + + + ); +}; + +export default SignedBadgeIcon; diff --git a/src/models.ts b/src/models.ts index 620b3feb..2938c73a 100644 --- a/src/models.ts +++ b/src/models.ts @@ -44,9 +44,9 @@ export const RepositoryModel = { apiVersion: 'v1alpha1', label: 'Repository', // t('Repository') - labelKey: 'plugin__pipelines-console-plugin~Repository', + labelKey: 'Repository', // t('Repositories') - labelPluralKey: 'plugin__pipelines-console-plugin~Repositories', + labelPluralKey: 'Repositories', plural: 'repositories', abbr: 'R', namespaced: true, diff --git a/src/types/index.ts b/src/types/index.ts index 941e50a7..9b670469 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,6 +20,7 @@ export * from './openshift'; export * from './pipeline'; export * from './pipelineResource'; export * from './pipelineRun'; +export * from './repository'; export * from './task'; export * from './taskRun'; export * from './triggers'; diff --git a/src/types/repository.ts b/src/types/repository.ts new file mode 100644 index 00000000..f0a01968 --- /dev/null +++ b/src/types/repository.ts @@ -0,0 +1,23 @@ +import { K8sResourceKind } from '@openshift-console/dynamic-plugin-sdk'; +import { Condition } from './pipelineRun'; + +export type RepositoryStatus = { + completionTime?: string; + conditions?: Condition[]; + logurl?: string; + pipelineRunName: string; + sha?: string; + startTime?: string; + title?: string; + event_type?: string; + target_branch?: string; +}; + +export type RepositoryKind = K8sResourceKind & { + spec?: { + url: string; + branch?: string; + namespace?: string; + }; + pipelinerun_status?: RepositoryStatus[]; +};