diff --git a/codecv.yml b/codecv.yml index 3c36ee96..dc965d54 100644 --- a/codecv.yml +++ b/codecv.yml @@ -9,11 +9,10 @@ coverage: project: default: target: auto - threshold: 0.2% + threshold: 1% patch: default: - target: 80% # coverage for new/modified code - threshold: 1% + informational: true ignore: - "**/*__data__*/*.ts" diff --git a/src/components/ApplicationDetails/tabs/overview/visualization/hooks/useAppWorkflowData.ts b/src/components/ApplicationDetails/tabs/overview/visualization/hooks/useAppWorkflowData.ts index b7685783..2c1663ed 100644 --- a/src/components/ApplicationDetails/tabs/overview/visualization/hooks/useAppWorkflowData.ts +++ b/src/components/ApplicationDetails/tabs/overview/visualization/hooks/useAppWorkflowData.ts @@ -14,7 +14,6 @@ import { useAppApplicationTestNodes } from './useAppApplicationTestNodes'; import { useAppBuildNodes } from './useAppBuildNodes'; import { useAppComponentsNodes } from './useAppComponentsNodes'; import { useAppReleaseNodes } from './useAppReleaseNodes'; -import { useAppReleasePlanNodes } from './useAppReleasePlanNodes'; export const useAppWorkflowData = ( applicationName: string, @@ -63,35 +62,17 @@ export const useAppWorkflowData = ( applicationIntegrationTestNodes, ]); - const [releaseNodes, releaseGroup, releaseTasks, releasesLoaded, releasesError] = - useAppReleaseNodes( - namespace, - applicationName, - expanded ? applicationIntegrationTestTasks : [testsGroup?.id ?? ''], - expanded, - ); - - const [ - managedEnvironmentNodes, - managedEnvironmentGroup, - managedEnvironmentsLoaded, - managedEnvironmentsError, - ] = useAppReleasePlanNodes(namespace, applicationName, releaseTasks, expanded); + const [releaseNodes, releaseGroup, , releasesLoaded, releasesError] = useAppReleaseNodes( + namespace, + applicationName, + expanded ? applicationIntegrationTestTasks : [testsGroup?.id ?? ''], + expanded, + ); const allResourcesLoaded: boolean = - componentsLoaded && - buildsLoaded && - applicationTestsLoaded && - releasesLoaded && - managedEnvironmentsLoaded; + componentsLoaded && buildsLoaded && applicationTestsLoaded && releasesLoaded; - const errors = [ - ...componentsErrors, - ...buildsErrors, - ...applicationErrors, - ...releasesError, - ...managedEnvironmentsError, - ]; + const errors = [...componentsErrors, ...buildsErrors, ...applicationErrors, ...releasesError]; if (!allResourcesLoaded || errors.length > 0) { return [{ nodes: [], edges: [] }, allResourcesLoaded, errors]; @@ -103,7 +84,6 @@ export const useAppWorkflowData = ( ...(buildNodes?.length ? buildNodes : [buildGroup]), ...applicationIntegrationTestNodes, ...(releaseNodes?.length ? releaseNodes : [releaseGroup]), - ...(managedEnvironmentNodes?.length ? managedEnvironmentNodes : [managedEnvironmentGroup]), ]; const spacerNodes = getSpacerNodes(resourceNodes, NodeType.SPACER_NODE); const nodes = [ @@ -113,13 +93,12 @@ export const useAppWorkflowData = ( buildGroup, testsGroup, releaseGroup, - managedEnvironmentGroup, ]; const edges = getEdgesFromNodes(nodes, NodeType.SPACER_NODE); return [{ nodes, edges }, true, errors]; } - const nodes = [componentGroup, buildGroup, testsGroup, releaseGroup, managedEnvironmentGroup]; + const nodes = [componentGroup, buildGroup, testsGroup, releaseGroup]; const edges = getEdgesFromNodes(nodes, NodeType.SPACER_NODE); return [{ nodes, edges }, true, errors]; diff --git a/src/components/ApplicationDetails/tabs/overview/visualization/utils/node-utils.ts b/src/components/ApplicationDetails/tabs/overview/visualization/utils/node-utils.ts index 67e80965..3ae172af 100644 --- a/src/components/ApplicationDetails/tabs/overview/visualization/utils/node-utils.ts +++ b/src/components/ApplicationDetails/tabs/overview/visualization/utils/node-utils.ts @@ -114,23 +114,14 @@ export const getLinkDataForElement = ( : { tab: 'integrationtests', }; - case WorkflowNodeType.STATIC_ENVIRONMENT: - return { - tab: 'deployments', - filter: - !groupNode && !isDisabled - ? { name: 'name', value: label } - : { name: 'envType', value: 'default,static' }, - }; - case WorkflowNodeType.MANAGED_ENVIRONMENT: case WorkflowNodeType.RELEASE: - return { - tab: 'deployments', - filter: - !groupNode && !isDisabled - ? { name: 'name', value: label } - : { name: 'envType', value: 'managed' }, - }; + return !groupNode && !isDisabled + ? { + path: `/workspaces/${workspace}/applications/${ + element.getData().application + }/releases/${label}`, + } + : { tab: 'releases' }; default: return { tab: 'overview', diff --git a/src/components/Applications/ApplicationListView.tsx b/src/components/Applications/ApplicationListView.tsx index fbf6054a..1661b30b 100644 --- a/src/components/Applications/ApplicationListView.tsx +++ b/src/components/Applications/ApplicationListView.tsx @@ -5,16 +5,20 @@ import { EmptyStateBody, PageSection, PageSectionVariants, + SearchInput, Spinner, Toolbar, ToolbarContent, ToolbarItem, } from '@patternfly/react-core'; +import { debounce } from 'lodash-es'; import emptyStateImgUrl from '../../assets/Application.svg'; import { useApplications } from '../../hooks/useApplications'; +import { useSearchParam } from '../../hooks/useSearchParam'; import { ApplicationModel, ComponentModel } from '../../models'; import { Table } from '../../shared'; import AppEmptyState from '../../shared/components/empty-state/AppEmptyState'; +import FilteredEmptyState from '../../shared/components/empty-state/FilteredEmptyState'; import { ApplicationKind } from '../../types'; import { useApplicationBreadcrumbs } from '../../utils/breadcrumb-utils'; import { useAccessReviewForModel } from '../../utils/rbac'; @@ -29,12 +33,29 @@ const ApplicationListView: React.FC> = () => { const applicationBreadcrumbs = useApplicationBreadcrumbs(); const [canCreateApplication] = useAccessReviewForModel(ApplicationModel, 'create'); const [canCreateComponent] = useAccessReviewForModel(ComponentModel, 'create'); + const [nameFilter, setNameFilter] = useSearchParam('name', ''); const [applications, loaded] = useApplications(namespace, workspace); applications?.sort( (app1, app2) => +new Date(app2.metadata?.creationTimestamp) - +new Date(app1.metadata?.creationTimestamp), ); + const filteredApplications = React.useMemo(() => { + const lowerCaseNameFilter = nameFilter.toLowerCase(); + return applications?.filter( + (app) => + app.spec.displayName?.toLowerCase().includes(lowerCaseNameFilter) ?? + app.metadata.name.includes(lowerCaseNameFilter), + ); + }, [nameFilter, applications]); + + const onClearFilters = () => { + setNameFilter(''); + }; + + const onNameInput = debounce((n: string) => { + setNameFilter(n); + }, 600); if (!loaded) { return ( @@ -86,6 +107,17 @@ const ApplicationListView: React.FC> = () => { <> + + onNameInput(n)} + value={nameFilter} + /> + > = () => { - ({ - id: obj.metadata?.name, - })} - /> + {filteredApplications.length !== 0 ? ( +
({ + id: obj.metadata?.name, + })} + /> + ) : ( + + )} )} diff --git a/src/components/Applications/__tests__/ApplicationListView.spec.tsx b/src/components/Applications/__tests__/ApplicationListView.spec.tsx index 6797f98a..08c32745 100644 --- a/src/components/Applications/__tests__/ApplicationListView.spec.tsx +++ b/src/components/Applications/__tests__/ApplicationListView.spec.tsx @@ -1,10 +1,14 @@ -import { screen } from '@testing-library/react'; +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Table as PfTable, TableHeader } from '@patternfly/react-table/deprecated'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import { ApplicationKind } from '../../../types'; import { createK8sWatchResourceMock, createUseWorkspaceInfoMock, renderWithQueryClient, } from '../../../utils/test-utils'; +import ApplicationListRow from '../ApplicationListRow'; import ApplicationListView from '../ApplicationListView'; jest.mock('react-i18next', () => ({ @@ -16,6 +20,7 @@ jest.mock('react-router-dom', () => { return { ...actual, useLocation: jest.fn(() => ({})), + useSearchParams: jest.fn(), Link: (props) => {props.children}, useNavigate: jest.fn(), }; @@ -25,6 +30,30 @@ jest.mock('../../../utils/rbac', () => ({ useAccessReviewForModel: jest.fn(() => [true, true]), })); +jest.mock('../../../shared/components/table', () => { + const actual = jest.requireActual('../../../shared/components/table'); + return { + ...actual, + Table: (props) => { + const { data, filters, selected, match, kindObj } = props; + const cProps = { data, filters, selected, match, kindObj }; + const columns = props.Header(cProps); + return ( + + + + {props.data.map((d, i) => ( + + + + ))} + + + ); + }, + }; +}); + const applications: ApplicationKind[] = [ { kind: 'Application', @@ -88,12 +117,17 @@ const applications: ApplicationKind[] = [ }, ]; +const useSearchParamsMock = useSearchParams as jest.Mock; const watchResourceMock = createK8sWatchResourceMock(); createUseWorkspaceInfoMock({ namespace: 'test-ns', workspace: 'test-ws' }); const ApplicationList = ApplicationListView; describe('Application List', () => { + beforeEach(() => { + useSearchParamsMock.mockImplementation(() => React.useState(new URLSearchParams())); + }); + it('should render spinner if application data is not loaded', () => { watchResourceMock.mockReturnValue([[], false]); renderWithQueryClient(); @@ -130,4 +164,116 @@ describe('Application List', () => { renderWithQueryClient(); expect(screen.queryByTestId('applications-breadcrumb-link')).not.toBeInTheDocument(); }); + + it('should apply query params to the filter', async () => { + watchResourceMock.mockReturnValue([applications, true]); + useSearchParamsMock.mockImplementation(() => + React.useState(new URLSearchParams('name=xyz-app')), + ); + renderWithQueryClient(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + }); + + it('should filter applications by name', async () => { + watchResourceMock.mockReturnValue([applications, true]); + const { rerender } = renderWithQueryClient(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + + const filter = screen.getByPlaceholderText('Filter by name...'); + fireEvent.change(filter, { target: { value: 'xyz-app' } }); + rerender(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + }); + + it('should use fallback filter value of metadata.name', async () => { + const alteredApplications = applications.map((app) => ({ + ...app, + spec: { displayName: undefined }, + })); + watchResourceMock.mockReturnValue([alteredApplications, true]); + const { rerender } = renderWithQueryClient(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + + const filter = screen.getByPlaceholderText('Filter by name...'); + fireEvent.change(filter, { target: { value: 'xyz-app' } }); + rerender(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + }); + + it('should filter case insensitive', async () => { + watchResourceMock.mockReturnValue([applications, true]); + const { rerender } = renderWithQueryClient(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + + const filter = screen.getByPlaceholderText('Filter by name...'); + fireEvent.change(filter, { target: { value: 'XYZ' } }); + rerender(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + }); + + it('should clear the filter when clear button is clicked', async () => { + watchResourceMock.mockReturnValue([applications, true]); + const { rerender } = renderWithQueryClient(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + + const filter = screen.getByPlaceholderText('Filter by name...'); + fireEvent.change(filter, { target: { value: 'invalid-app' } }); + rerender(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).not.toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).not.toBeInTheDocument(); + expect(screen.queryByText('No results found')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Clear all filters' })); + rerender(); + await waitFor(() => { + expect(screen.queryByText('mno-app')).toBeInTheDocument(); + expect(screen.queryByText('mno-app1')).toBeInTheDocument(); + expect(screen.queryByText('mno-app2')).toBeInTheDocument(); + expect(screen.queryByText('xyz-app')).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Commits/CommitDetails/visualization/__tests__/CommitVisualization.spec.tsx b/src/components/Commits/CommitDetails/visualization/__tests__/CommitVisualization.spec.tsx index be8c15eb..627e2fac 100644 --- a/src/components/Commits/CommitDetails/visualization/__tests__/CommitVisualization.spec.tsx +++ b/src/components/Commits/CommitDetails/visualization/__tests__/CommitVisualization.spec.tsx @@ -120,6 +120,6 @@ describe('CommitVisualization', () => { const nodes = graph.querySelectorAll('[data-kind="node"]'); - expect(nodes).toHaveLength(8); + expect(nodes).toHaveLength(6); }); }); diff --git a/src/components/Commits/CommitDetails/visualization/useCommitWorkflowData.ts b/src/components/Commits/CommitDetails/visualization/useCommitWorkflowData.ts index 78d9bcba..ba4cf78a 100644 --- a/src/components/Commits/CommitDetails/visualization/useCommitWorkflowData.ts +++ b/src/components/Commits/CommitDetails/visualization/useCommitWorkflowData.ts @@ -1,22 +1,10 @@ import * as React from 'react'; -import { - PipelineRunEventType, - PipelineRunLabel, - PipelineRunType, -} from '../../../../consts/pipelinerun'; +import { PipelineRunLabel, PipelineRunType } from '../../../../consts/pipelinerun'; import { useComponents } from '../../../../hooks/useComponents'; import { useIntegrationTestScenarios } from '../../../../hooks/useIntegrationTestScenarios'; import { usePipelineRunsForCommit } from '../../../../hooks/usePipelineRuns'; -import { useReleasePlans } from '../../../../hooks/useReleasePlans'; -import { useReleases } from '../../../../hooks/useReleases'; -import { useSnapshots } from '../../../../hooks/useSnapshots'; import { Commit, ComponentKind, PipelineRunKind } from '../../../../types'; -import { ReleaseKind, ReleasePlanKind } from '../../../../types/coreBuildService'; -import { - conditionsRunStatus, - pipelineRunStatus, - runStatus, -} from '../../../../utils/pipeline-utils'; +import { pipelineRunStatus, runStatus } from '../../../../utils/pipeline-utils'; import { DEFAULT_NODE_HEIGHT } from '../../../topology/const'; import { getLabelWidth } from '../../../topology/utils'; import { useWorkspaceInfo } from '../../../Workspace/useWorkspaceInfo'; @@ -41,7 +29,6 @@ export const useCommitWorkflowData = ( commit: Commit, ): [nodes: CommitWorkflowNodeModel[], loaded: boolean, errors: unknown[]] => { const { namespace, workspace } = useWorkspaceInfo(); - const [mvpFeature] = [false]; const applicationName = commit?.application || ''; const [components, componentsLoaded] = useComponents(namespace, workspace, applicationName); @@ -50,11 +37,6 @@ export const useCommitWorkflowData = ( workspace, applicationName, ); - const [releasePlans, releasePlansLoaded, releasePlansError] = useReleasePlans( - namespace, - workspace, - ); - const [releases, releasesLoaded, releasesError] = useReleases(namespace, workspace); const [pipelines, pipelinesLoaded, pipelinesError] = usePipelineRunsForCommit( namespace, workspace, @@ -81,16 +63,8 @@ export const useCommitWorkflowData = ( [pipelines, pipelinesLoaded], ); - const [snapshots, sloaded, serror] = useSnapshots(namespace, commit.sha); - - const allResourcesLoaded: boolean = - componentsLoaded && - integrationTestsLoaded && - pipelinesLoaded && - releasesLoaded && - sloaded && - releasePlansLoaded; - const allErrors = [releasePlansError, releasesError, pipelinesError, serror].filter((e) => !!e); + const allResourcesLoaded: boolean = componentsLoaded && integrationTestsLoaded && pipelinesLoaded; + const allErrors = [pipelinesError].filter((e) => !!e); const commitComponents = React.useMemo( () => @@ -203,120 +177,6 @@ export const useCommitWorkflowData = ( nodes.push(...appTestNodes); const appTestNodesWidth = appTestNodes.reduce((max, node) => Math.max(max, node.width), 0); appTestNodes.forEach((n) => (n.width = appTestNodesWidth)); - - const currentSnapshotName = getLatestResource( - snapshots.filter( - (s) => - s.metadata.labels[PipelineRunLabel.COMPONENT] === compName && - s.metadata.labels[PipelineRunLabel.TEST_SERVICE_EVENT_TYPE_LABEL] === - PipelineRunEventType.PUSH, - ), - )?.metadata?.name; - - if (!mvpFeature) { - const latestRelease: ReleaseKind = getLatestResource( - releases.filter((r) => r.spec.snapshot === currentSnapshotName), - ); - - const releaseStatus: runStatus = - releases.length === 0 - ? undefined - : latestRelease && latestRelease?.status - ? conditionsRunStatus(latestRelease.status.conditions) - : runStatus.Succeeded; - - const releaseNodes: CommitWorkflowNodeModel[] = releases.length - ? releases.map((release) => { - const releaseName = release.metadata.name; - - const releaseNode: CommitWorkflowNodeModel = { - id: addPrefixToResourceName(compName, releaseName), - label: releaseName, - type: NodeType.WORKFLOW_NODE, - width: getLabelWidth(releaseName), - height: DEFAULT_NODE_HEIGHT, - data: { - status: releaseStatus, - workflowType: CommitWorkflowNodeType.RELEASE, - resource: release, - application: commit.application, - }, - }; - return releaseNode; - }) - : [ - { - id: `${name}-release`, - label: 'No releases set', - type: NodeType.WORKFLOW_NODE, - width: getLabelWidth('No releases set'), - height: DEFAULT_NODE_HEIGHT, - data: { - status: runStatus.Pending, - workflowType: CommitWorkflowNodeType.RELEASE, - application: commit.application, - }, - }, - ]; - nodes.push(...releaseNodes); - const releaseNodesWidth = releaseNodes.reduce((max, node) => Math.max(max, node.width), 0); - releaseNodes.forEach((n) => (n.width = releaseNodesWidth)); - const releaseNodeIds = releaseNodes.map((n) => n.id); - - const releasePlanStatus: (rp: ReleasePlanKind) => runStatus = - releasePlans.length === 0 - ? undefined - : (rp) => { - const matchedRelease = getLatestResource( - releases.filter((r) => r.spec.releasePlan === rp.metadata.name), - ); - return matchedRelease - ? pipelineRunStatus(matchedRelease as PipelineRunKind) - : runStatus.Pending; - }; - - const managedEnvNodes: CommitWorkflowNodeModel[] = releasePlans.length - ? releasePlans.map((managedEnv) => { - const managedEnvName = managedEnv.metadata.name; - - const managedEnvNode: CommitWorkflowNodeModel = { - id: addPrefixToResourceName(compName, managedEnvName), - label: managedEnvName, - type: NodeType.WORKFLOW_NODE, - width: getLabelWidth(managedEnvName), - height: DEFAULT_NODE_HEIGHT, - runAfterTasks: releaseNodeIds, - data: { - status: releasePlanStatus(managedEnv), - workflowType: CommitWorkflowNodeType.MANAGED_ENVIRONMENT, - resource: managedEnv, - application: commit.application, - }, - }; - return managedEnvNode; - }) - : [ - { - id: `${name}-managed-environments`, - label: 'No managed environments set', - type: NodeType.WORKFLOW_NODE, - width: getLabelWidth('No managed environments set'), - height: DEFAULT_NODE_HEIGHT, - runAfterTasks: releaseNodeIds, - data: { - status: runStatus.Pending, - workflowType: CommitWorkflowNodeType.MANAGED_ENVIRONMENT, - application: commit.application, - }, - }, - ]; - nodes.push(...managedEnvNodes); - const managedEnvNodesWidth = managedEnvNodes.reduce( - (max, node) => Math.max(max, node.width), - 0, - ); - managedEnvNodes.forEach((n) => (n.width = managedEnvNodesWidth)); - } }); return nodes; @@ -329,10 +189,6 @@ export const useCommitWorkflowData = ( buildPipelines, testPipelines, integrationTests, - snapshots, - mvpFeature, - releases, - releasePlans, ]); if (!allResourcesLoaded || workflowNodes.length === 0 || allErrors.length > 0) { diff --git a/src/components/ImportForm/ComponentSection/GitOptions.tsx b/src/components/ImportForm/ComponentSection/GitOptions.tsx index 58e29cd8..33730fb1 100644 --- a/src/components/ImportForm/ComponentSection/GitOptions.tsx +++ b/src/components/ImportForm/ComponentSection/GitOptions.tsx @@ -14,7 +14,7 @@ const GitOptions: React.FC> = () => { diff --git a/src/components/ImportForm/ComponentSection/SourceSection.tsx b/src/components/ImportForm/ComponentSection/SourceSection.tsx index f2d81a37..04256557 100644 --- a/src/components/ImportForm/ComponentSection/SourceSection.tsx +++ b/src/components/ImportForm/ComponentSection/SourceSection.tsx @@ -41,7 +41,7 @@ export const SourceSection = () => { onChange={handleChange} /> {validated === ValidatedOptions.success ? ( - + ) : null} {validated === ValidatedOptions.success ? : null} diff --git a/src/components/ImportForm/ComponentSection/__tests__/ComponentSection.spec.tsx b/src/components/ImportForm/ComponentSection/__tests__/ComponentSection.spec.tsx index 4de4b261..fe057b9b 100644 --- a/src/components/ImportForm/ComponentSection/__tests__/ComponentSection.spec.tsx +++ b/src/components/ImportForm/ComponentSection/__tests__/ComponentSection.spec.tsx @@ -22,4 +22,19 @@ describe('ComponentSection', () => { await user.tab(); await waitFor(() => screen.getByText('Show advanced Git options')); }); + it('should get private image repo switch when git src is ready', async () => { + formikRenderer(, { + source: { git: { url: '' } }, + }); + const user = userEvent.setup(); + const source = screen.getByPlaceholderText('Enter your source'); + + await user.type(source, 'https://github.com/abcd/repo.git'); + await user.tab(); + + const switchCheckbox = screen.getByLabelText('Should the image produced be private?'); + expect(switchCheckbox).not.toBeChecked(); + await user.click(switchCheckbox); + expect(switchCheckbox).toBeChecked(); + }); }); diff --git a/src/components/ImportForm/__tests__/submit-utils.spec.ts b/src/components/ImportForm/__tests__/submit-utils.spec.ts index 2b6c3156..6e2a21e7 100644 --- a/src/components/ImportForm/__tests__/submit-utils.spec.ts +++ b/src/components/ImportForm/__tests__/submit-utils.spec.ts @@ -85,7 +85,7 @@ describe('Submit Utils: createResources', () => { application: 'test-app', inAppContext: true, showComponent: true, - isPrivateRepo: false, + isPrivateRepo: true, source: { git: { url: 'https://github.com/', diff --git a/src/components/Releases/ReleaseOverviewTab.tsx b/src/components/Releases/ReleaseOverviewTab.tsx index 2c2d38a5..3701d621 100644 --- a/src/components/Releases/ReleaseOverviewTab.tsx +++ b/src/components/Releases/ReleaseOverviewTab.tsx @@ -1,12 +1,14 @@ import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; import { + Bullseye, DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Flex, FlexItem, + Spinner, Title, } from '@patternfly/react-core'; import { useReleasePlan } from '../../hooks/useReleasePlans'; @@ -36,6 +38,14 @@ const ReleaseOverviewTab: React.FC = () => { ); const status = useReleaseStatus(release); + if (!releasePlanLoaded) { + return ( + + + + ); + } + return ( <> diff --git a/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx b/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx index 2cecef66..1d55dab2 100644 --- a/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx +++ b/src/components/Releases/__tests__/ReleaseOverviewTab.spec.tsx @@ -19,13 +19,18 @@ jest.mock('../../../hooks/useReleases', () => ({ const watchResourceMock = createK8sWatchResourceMock(); describe('ReleaseOverviewTab', () => { - beforeEach(() => { - watchResourceMock.mockReturnValue([{ spec: { application: 'test-app' } }, true]); - }); + beforeEach(() => {}); createUseWorkspaceInfoMock({ namespace: 'test-ns', workspace: 'test-ws' }); + it('should render loading indicator', () => { + watchResourceMock.mockReturnValue([{ spec: { application: 'test-app' } }, false]); + render(<ReleaseOverviewTab />); + expect(screen.getByRole('progressbar')).toBeVisible(); + }); + it('should render correct details', () => { + watchResourceMock.mockReturnValue([{ spec: { application: 'test-app' } }, true]); render(<ReleaseOverviewTab />); expect(screen.getByText('Duration')).toBeVisible(); expect(screen.getByText('10 seconds')).toBeVisible(); diff --git a/src/components/SnapshotDetails/SnapshotDetailsView.tsx b/src/components/SnapshotDetails/SnapshotDetailsView.tsx index 585e05b9..e0f531f0 100644 --- a/src/components/SnapshotDetails/SnapshotDetailsView.tsx +++ b/src/components/SnapshotDetails/SnapshotDetailsView.tsx @@ -7,7 +7,6 @@ import { useSnapshot } from '../../hooks/useSnapshots'; import { HttpError } from '../../k8s/error'; import { RouterParams } from '../../routes/utils'; import ErrorEmptyState from '../../shared/components/empty-state/ErrorEmptyState'; -import { LoadingBox } from '../../shared/components/status-box/StatusBox'; import { Timestamp } from '../../shared/components/timestamp/Timestamp'; import { useApplicationBreadcrumbs } from '../../utils/breadcrumb-utils'; import { createCommitObjectFromPLR } from '../../utils/commits-utils'; @@ -50,7 +49,11 @@ const SnapshotDetailsView: React.FC = () => { } if (!plrLoadError && !plrLoaded) { - return <LoadingBox />; + return ( + <Bullseye> + <Spinner size="lg" /> + </Bullseye> + ); } if (snapshot?.metadata) { diff --git a/src/components/SnapshotDetails/__tests__/SnapshotDetailsView.spec.tsx b/src/components/SnapshotDetails/__tests__/SnapshotDetailsView.spec.tsx index 238c52a6..16eba2f7 100644 --- a/src/components/SnapshotDetails/__tests__/SnapshotDetailsView.spec.tsx +++ b/src/components/SnapshotDetails/__tests__/SnapshotDetailsView.spec.tsx @@ -67,10 +67,10 @@ describe('SnapshotDetailsView', () => { (useCommitStatus as jest.Mock).mockReturnValueOnce(['-', true]); }); - it('should render spinner if test data is not loaded', () => { + it('should render loading indicator', () => { watchResourceMock.mockReturnValue([[], false]); renderWithQueryClientAndRouter(<SnapshotDetails />); - expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); + screen.getByRole('progressbar'); }); it('should show error state if test cannot be loaded', () => { diff --git a/src/hooks/__tests__/useApplicationReleases.spec.ts b/src/hooks/__tests__/useApplicationReleases.spec.ts index 19e3b1f2..2ef951d8 100644 --- a/src/hooks/__tests__/useApplicationReleases.spec.ts +++ b/src/hooks/__tests__/useApplicationReleases.spec.ts @@ -15,6 +15,32 @@ const watchResourceMock = createK8sWatchResourceMock(); const useSnapshotsMock = useApplicationSnapshots as jest.Mock; describe('useApplicationReleases', () => { + it('should return empty array incase release are not loaded', () => { + watchResourceMock.mockReturnValue([[], false]); + useSnapshotsMock.mockReturnValue([ + [{ metadata: { name: 'my-snapshot' } }, { metadata: { name: 'my-snapshot-2' } }], + true, + ]); + + const { result } = renderHook(() => useApplicationReleases('test-app')); + const [results, loaded] = result.current; + expect(loaded).toEqual(false); + expect(results.length).toEqual(0); + }); + + it('should return empty array incase snapshots are not loaded', () => { + watchResourceMock.mockReturnValue([[], true]); + useSnapshotsMock.mockReturnValue([ + [{ metadata: { name: 'my-snapshot' } }, { metadata: { name: 'my-snapshot-2' } }], + false, + ]); + + const { result } = renderHook(() => useApplicationReleases('test-app')); + const [results, loaded] = result.current; + expect(loaded).toEqual(false); + expect(results.length).toEqual(0); + }); + it('should only return releases that are in the application', () => { watchResourceMock.mockReturnValue([ [ diff --git a/src/hooks/useApplicationReleases.ts b/src/hooks/useApplicationReleases.ts index 5af58731..92d7ef37 100644 --- a/src/hooks/useApplicationReleases.ts +++ b/src/hooks/useApplicationReleases.ts @@ -26,8 +26,11 @@ export const useApplicationReleases = ( const [snapshots, snapshotsLoaded, snapshotsError] = useApplicationSnapshots(applicationName); const releasesForApp = React.useMemo( - () => releases.filter((r) => snapshots.some((s) => s.metadata.name === r.spec.snapshot)), - [releases, snapshots], + () => + !releasesLoaded && snapshotsLoaded + ? releases.filter((r) => snapshots.some((s) => s.metadata.name === r.spec.snapshot)) + : [], + [releases, releasesLoaded, snapshots, snapshotsLoaded], ); return [releasesForApp, !releasesLoaded && snapshotsLoaded, releasesError || snapshotsError]; diff --git a/src/hooks/useReleasePlans.ts b/src/hooks/useReleasePlans.ts index 40c3966a..642ad1e8 100644 --- a/src/hooks/useReleasePlans.ts +++ b/src/hooks/useReleasePlans.ts @@ -30,7 +30,6 @@ export const useReleasePlan = ( namespace, workspace, name, - isList: true, }, ReleasePlanModel, ); diff --git a/src/k8s/k8s-utils.ts b/src/k8s/k8s-utils.ts index c3519207..da57239c 100644 --- a/src/k8s/k8s-utils.ts +++ b/src/k8s/k8s-utils.ts @@ -237,6 +237,7 @@ export const k8sWatch = ( const path = getK8sResourceURL(kind, undefined, opts); wsOptionsUpdated.path = `/wss/k8s${path}`; wsOptionsUpdated.host = 'auto'; + wsOptionsUpdated.subProtocols = ['base64.binary.k8s.io']; return new WebSocketFactory(path, wsOptionsUpdated); }; diff --git a/src/utils/pipeline-utils.ts b/src/utils/pipeline-utils.ts index 64ab66ee..62492802 100644 --- a/src/utils/pipeline-utils.ts +++ b/src/utils/pipeline-utils.ts @@ -183,6 +183,7 @@ export const conditionsRunStatus = (conditions: Condition[], specStatus?: string } const cancelledCondition = conditions.find((c) => c.reason === 'Cancelled'); + const stoppingCondition = conditions.find((c) => c.reason === 'StoppedRunningFinally'); const succeedCondition = conditions.find((c) => c.type === 'Succeeded'); if (!succeedCondition || !succeedCondition.status) { @@ -197,11 +198,8 @@ export const conditionsRunStatus = (conditions: Condition[], specStatus?: string : runStatus.Running; if ( - [ - `${SucceedConditionReason.PipelineRunStopped}`, - `${SucceedConditionReason.PipelineRunCancelled}`, - ].includes(specStatus) && - !cancelledCondition + (specStatus === SucceedConditionReason.PipelineRunCancelled && !cancelledCondition) || + (specStatus === SucceedConditionReason.PipelineRunStopped && stoppingCondition) ) { return runStatus.Cancelling; }