Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createTableHeaders } from '~/shared/components/table/utils';

export const versionTableColumnClasses = {
name: 'pf-m-width-20',
revision: 'pf-m-width-20',
pipeline: 'pf-m-width-20',
};

export const enum SortableHeaders {
name = 0,
revision = 1,
}

const versionColumns = [
{ title: 'Version name', className: versionTableColumnClasses.name, sortable: true },
{ title: 'Git branch or tag', className: versionTableColumnClasses.revision, sortable: true },
{ title: 'Pipeline', className: versionTableColumnClasses.pipeline },
];

export default createTableHeaders(versionColumns);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { COMPONENT_VERSION_DETAILS_PATH } from '~/routes/paths';
import { TableData } from '~/shared';
import ExternalLink from '~/shared/components/links/ExternalLink';
import { useNamespace } from '~/shared/providers/Namespace';
import { ComponentBuildPipeline, ComponentVersion } from '~/types/component';
import { createBranchUrl } from '~/utils/git-utils';
import { versionTableColumnClasses } from './ComponentVersionListHeader';

export type VersionListRowCustomData = {
repoUrl?: string;
defaultPipeline?: ComponentBuildPipeline;
componentName: string;
};

const getPipelineName = (
versionPipeline?: ComponentBuildPipeline,
defaultPipeline?: ComponentBuildPipeline,
): string => {
const pipeline = versionPipeline ?? defaultPipeline;
if (!pipeline) {
return '-';
}

const def = pipeline['pull-and-push'] ?? pipeline.push ?? pipeline.pull;
if (!def) {
return '-';
}

return def['pipelineref-by-name'] ?? def['pipelinespec-from-bundle']?.name ?? '-';
};

interface ComponentVersionListRowProps {
obj: ComponentVersion;
customData: VersionListRowCustomData;
}

export const ComponentVersionListRow: React.FC<ComponentVersionListRowProps> = ({
obj,
customData,
}) => {
const namespace = useNamespace();
const { repoUrl, defaultPipeline, componentName } = customData;
const branchUrl = createBranchUrl(repoUrl, obj.revision);
const pipelineName = getPipelineName(obj['build-pipeline'], defaultPipeline);

return (
<>
<TableData className={versionTableColumnClasses.name}>
<Link
to={COMPONENT_VERSION_DETAILS_PATH.createPath({
workspaceName: namespace,
componentName,
versionRevision: obj.revision,
})}
>
{obj.name}
</Link>
</TableData>
<TableData className={versionTableColumnClasses.revision}>
{branchUrl ? <ExternalLink href={branchUrl} text={obj.revision} /> : obj.revision || '-'}
</TableData>
<TableData className={versionTableColumnClasses.pipeline}>{pipelineName}</TableData>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { SortByDirection } from '@patternfly/react-table';
import { FilterContext } from '~/components/Filter/generic/FilterContext';
import { BaseTextFilterToolbar } from '~/components/Filter/toolbars/BaseTextFIlterToolbar';
import { useComponent } from '~/hooks/useComponents';
import { useSortedResources } from '~/hooks/useSortedResources';
import { Table } from '~/shared';
import FilteredEmptyState from '~/shared/components/empty-state/FilteredEmptyState';
import { useNamespace } from '~/shared/providers/Namespace';
import { getErrorState } from '~/shared/utils/error-utils';
import { ComponentVersion } from '~/types/component';
import getVersionListHeader, { SortableHeaders } from './ComponentVersionListHeader';
import { ComponentVersionListRow, VersionListRowCustomData } from './ComponentVersionListRow';

type ComponentVersionListViewProps = {
componentName: string;
};

const sortPaths: Record<SortableHeaders, string> = {
[SortableHeaders.name]: 'name',
[SortableHeaders.revision]: 'revision',
};

const ComponentVersionListView: React.FC<
React.PropsWithChildren<ComponentVersionListViewProps>
> = ({ componentName }) => {
const namespace = useNamespace();
const { filters: unparsedFilters, setFilters, onClearFilters } = React.useContext(FilterContext);
const nameFilter = unparsedFilters.name ? (unparsedFilters.name as string) : '';

const [component, compLoaded, compError] = useComponent(namespace, componentName);

const [activeSortIndex, setActiveSortIndex] = React.useState<number>(SortableHeaders.name);
const [activeSortDirection, setActiveSortDirection] = React.useState<SortByDirection>(
SortByDirection.asc,
);

const versions = React.useMemo(
() => component?.spec?.source?.versions ?? [],
[component?.spec?.source?.versions],
);

const filteredVersions = React.useMemo(
() => versions.filter((v) => v.name.toLowerCase().includes(nameFilter.trim().toLowerCase())),
[versions, nameFilter],
);

const sortedVersions = useSortedResources(
filteredVersions,
activeSortIndex,
activeSortDirection,
sortPaths,
);

const repoUrl = component?.spec?.source?.url;
const defaultPipeline = component?.spec?.['default-build-pipeline'];

const customData = React.useMemo<VersionListRowCustomData>(
() => ({ repoUrl, defaultPipeline, componentName }),
[repoUrl, defaultPipeline, componentName],
);

const Header = React.useMemo(
() =>
getVersionListHeader(activeSortIndex, activeSortDirection, (_, index, direction) => {
setActiveSortIndex(index);
setActiveSortDirection(direction);
}),
[activeSortIndex, activeSortDirection],
);

const EmptyMsg = React.useCallback(
() => <FilteredEmptyState onClearFilters={() => onClearFilters()} />,
[onClearFilters],
);

if (compError) {
return getErrorState(compError, compLoaded, 'Component versions');
}

const isFiltered = nameFilter.length > 0;

return (
<>
{(isFiltered || versions.length > 0) && (
<BaseTextFilterToolbar
text={nameFilter}
label="name"
setText={(newName) => setFilters({ name: newName })}
onClearFilters={onClearFilters}
dataTest="version-list-toolbar"
/>
)}
<Table
data={sortedVersions}
unfilteredData={versions}
EmptyMsg={EmptyMsg}
aria-label="Component Version List"
Header={Header}
Row={(props) => (
<ComponentVersionListRow obj={props.obj as ComponentVersion} customData={customData} />
)}
loaded={compLoaded}
/>
</>
);
};

export default ComponentVersionListView;
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { screen } from '@testing-library/react';
import { ComponentVersion } from '~/types/component';
import { mockUseNamespaceHook } from '~/unit-test-utils/mock-namespace';
import { renderWithQueryClient } from '~/unit-test-utils/mock-react-query';
import { ComponentVersionListRow, VersionListRowCustomData } from '../ComponentVersionListRow';

jest.mock('react-router-dom', () => ({
Link: (props) => <a href={props.to}>{props.children}</a>,
}));

const mockVersion: ComponentVersion = {
name: 'Version 1.0',
revision: 'ver-1.0',
context: './frontend',
};

const mockVersionNoPipeline: ComponentVersion = {
name: 'Test',
revision: 'test',
};

const mockVersionWithPipeline: ComponentVersion = {
name: 'Custom Pipeline',
revision: 'custom-branch',
context: './backend',
'build-pipeline': {
'pull-and-push': {
'pipelineref-by-name': 'my-custom-pipeline',
},
},
};

const defaultCustomData: VersionListRowCustomData = {
repoUrl: 'https://github.com/org/repo',
componentName: 'my-component',
};

describe('ComponentVersionListRow', () => {
mockUseNamespaceHook('test-ns');

it('should render version name as a link', () => {
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersion} customData={defaultCustomData} />,
);
const link = screen.getByText('Version 1.0');
expect(link).toBeInTheDocument();
expect(link.closest('a')).toHaveAttribute('href', expect.stringContaining('ver-1.0'));
});

it('should render revision as an external link for GitHub repos', () => {
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersion} customData={defaultCustomData} />,
);
const externalLink = screen.getByText('ver-1.0');
expect(externalLink.closest('a')).toHaveAttribute(
'href',
'https://github.com/org/repo/tree/ver-1.0',
);
});

it('should render revision as plain text when repo URL is unknown provider', () => {
const customData: VersionListRowCustomData = {
repoUrl: 'https://unknown-git.example.com/org/repo',
componentName: 'my-component',
};
renderWithQueryClient(<ComponentVersionListRow obj={mockVersion} customData={customData} />);
expect(screen.getByText('ver-1.0')).toBeInTheDocument();
});

it('should render revision as plain text when repo URL is not set', () => {
const customData: VersionListRowCustomData = {
componentName: 'my-component',
};
renderWithQueryClient(<ComponentVersionListRow obj={mockVersion} customData={customData} />);
expect(screen.getByText('ver-1.0')).toBeInTheDocument();
});

it('should render pipeline name from version build-pipeline', () => {
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersionWithPipeline} customData={defaultCustomData} />,
);
expect(screen.getByText('my-custom-pipeline')).toBeInTheDocument();
});

it('should render pipeline name from default-build-pipeline when version has none', () => {
const customData: VersionListRowCustomData = {
repoUrl: 'https://github.com/org/repo',
componentName: 'my-component',
defaultPipeline: {
'pull-and-push': {
'pipelinespec-from-bundle': {
name: 'docker-build-oci-ta',
bundle: 'latest',
},
},
},
};
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersionNoPipeline} customData={customData} />,
);
expect(screen.getByText('docker-build-oci-ta')).toBeInTheDocument();
});

it('should render "-" when no pipeline is configured', () => {
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersionNoPipeline} customData={defaultCustomData} />,
);
expect(screen.getByText('-')).toBeInTheDocument();
});

it('should prefer version pipeline over default pipeline', () => {
const customData: VersionListRowCustomData = {
repoUrl: 'https://github.com/org/repo',
componentName: 'my-component',
defaultPipeline: {
'pull-and-push': {
'pipelinespec-from-bundle': {
name: 'default-pipeline',
bundle: 'latest',
},
},
},
};
renderWithQueryClient(
<ComponentVersionListRow obj={mockVersionWithPipeline} customData={customData} />,
);
expect(screen.getByText('my-custom-pipeline')).toBeInTheDocument();
expect(screen.queryByText('default-pipeline')).not.toBeInTheDocument();
});
});
Loading