Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show helm release revisions in history tab #4817

Merged
merged 4 commits into from
Mar 27, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Table, TextFilter } from '@console/internal/components/factory';
import { CheckBoxes } from '@console/internal/components/row-filter';
import { getQueryArgument } from '@console/internal/components/utils';
import { CustomResourceListProps } from './custom-resource-list-types';

const CustomResourceList: React.FC<CustomResourceListProps> = ({
dependentResource,
fetchCustomResources,
queryArg,
rowFilters,
rowFilterReducer,
textFilterReducer,
resourceHeader,
resourceRow,
sortBy,
sortOrder,
}) => {
const [listItems, setListItems] = React.useState([]);
const [filteredListItems, setFilteredListItems] = React.useState([]);
const [fetched, setFetched] = React.useState(false);

React.useEffect(() => {
let ignore = false;

const queryArgument = getQueryArgument(queryArg);
const activeFilters = queryArgument?.split(',');

const fetchListItems = async () => {
let newListItems: any;
try {
newListItems = await fetchCustomResources();
} catch {
if (ignore) return;

setListItems([]);
setFetched(true);
}

if (ignore) return;

setListItems(newListItems || []);
setFetched(true);

if (activeFilters) {
const filteredItems = rowFilterReducer(newListItems, activeFilters);
setFilteredListItems(filteredItems);
} else {
setFilteredListItems(newListItems);
}
};

fetchListItems();

return () => {
ignore = true;
};
}, [dependentResource, fetchCustomResources, queryArg, rowFilters, rowFilterReducer]);

const applyRowFilter = React.useCallback(
(filter) => {
const filteredItems = rowFilterReducer(listItems, filter);
setFilteredListItems(filteredItems);
},
[listItems, rowFilterReducer],
);

const applyTextFilter = React.useCallback(
(filter) => {
const filteredItems = textFilterReducer(listItems, filter);
setFilteredListItems(filteredItems);
},
[listItems, textFilterReducer],
);

const rowsOfRowFilters = _.map(
rowFilters,
({ items: filterItems, reducer, selected, type }, i) => {
return (
<CheckBoxes
key={i}
onFilterChange={applyRowFilter}
items={filterItems}
itemCount={_.size(listItems)}
numbers={_.countBy(listItems, reducer)}
selected={selected}
type={type}
reduxIDs={[]}
/>
);
},
);

return (
<>
<div className="co-m-pane__filter-bar">
<div className="co-m-pane__filter-bar-group co-m-pane__filter-bar-group--filter">
<TextFilter label="by name" onChange={(e) => applyTextFilter(e.target.value)} />
</div>
</div>

<div className="co-m-pane__body">
{!_.isEmpty(listItems) && rowsOfRowFilters}
<Table
data={filteredListItems}
defaultSortField={sortBy}
defaultSortOrder={sortOrder}
aria-label="CustomResources"
Header={resourceHeader}
Row={resourceRow}
loaded={fetched}
virtualize
/>
</div>
</>
);
};

export default React.memo(CustomResourceList);
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import * as fuzzy from 'fuzzysearch';
import { SortByDirection, sortable } from '@patternfly/react-table';
import { TableRow, TableData, Table, TextFilter } from '@console/internal/components/factory';
import CustomResourceList from '../CustomResourceList';
import {
CustomResourceListProps,
CustomResourceListRowFilter,
CustomResourceListRowProps,
} from '../custom-resource-list-types';

let customResourceListProps: CustomResourceListProps;

const mockColumnClasses = {
name: 'col-lg-4',
version: 'col-lg-4',
status: 'col-lg-4',
};

const MockTableHeader = () => {
return [
{
title: 'Name',
sortField: 'name',
transforms: [sortable],
props: { className: mockColumnClasses.name },
},
{
title: 'Version',
sortField: 'version',
transforms: [sortable],
props: { className: mockColumnClasses.version },
},
{
title: 'Status',
sortField: 'status',
transforms: [sortable],
props: { className: mockColumnClasses.status },
},
];
};

const MockTableRow: React.FC<CustomResourceListRowProps> = ({ obj, index, key, style }) => (
<TableRow id={obj.name} index={index} trKey={key} style={style}>
<TableData className={mockColumnClasses.name}>{obj.name}</TableData>
<TableData className={mockColumnClasses.version}>{obj.version}</TableData>
<TableData className={mockColumnClasses.status}>{obj.status}</TableData>
</TableRow>
);

// Couldn't test scenarios that work around useEffect becuase it seems there is no way to trigger useEffect from within the tests.
// More tests will be added once we find a way to do so. All the required mock-data is already added.
describe('CustomeResourceList', () => {
const mockReducer = (item) => {
return item.status;
};

const getItems = () => {
const items = [
{ name: 'item1', version: '1', status: 'successful' },
{
name: 'item2',
version: '2',
status: 'successful',
},
{ name: 'item3', version: '3', status: 'failed' },
{
name: 'item4',
version: '4',
status: 'failed',
},
];
return Promise.resolve(items);
};

const getFilteredItemsByRow = (items: any, filters: string[]) => {
return items.filter((item) => {
return filters.includes(item.status);
});
};

const getFilteredItemsByText = (items: any, filter: string) => {
return items.filter((item) => fuzzy(filter, item.name));
};

const mockSelectedStatuses = ['successful', 'failed'];

const mockRowFilters: CustomResourceListRowFilter[] = [
{
type: 'mock-filter',
selected: mockSelectedStatuses,
reducer: mockReducer,
items: mockSelectedStatuses.map((status) => ({
id: status,
title: status,
})),
},
];

customResourceListProps = {
queryArg: '',
fetchCustomResources: getItems,
rowFilters: mockRowFilters,
sortBy: 'version',
sortOrder: SortByDirection.desc,
rowFilterReducer: getFilteredItemsByRow,
textFilterReducer: getFilteredItemsByText,
resourceRow: MockTableRow,
resourceHeader: MockTableHeader,
};

const customResourceList = shallow(<CustomResourceList {...customResourceListProps} />);
it('should render Table component', () => {
expect(customResourceList.find(Table).exists()).toBe(true);
});

it('should render TextFilter component', () => {
expect(customResourceList.find(TextFilter).exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SortByDirection } from '@patternfly/react-table';

export interface CustomResourceListRowFilter {
type: string;
selected: string[];
reducer: (item: { [key: string]: any }) => string;
items: { [key: string]: any }[];
}

export interface CustomResourceListRowProps {
obj: { [key: string]: any };
index: number;
key?: string;
style: object;
}

export interface CustomResourceListProps {
queryArg: string;
rowFilters: CustomResourceListRowFilter[];
sortBy: string;
sortOrder: SortByDirection;
resourceRow: React.ComponentType<CustomResourceListRowProps>;
dependentResource?: any;
resourceHeader: () => { [key: string]: any }[];
fetchCustomResources: () => Promise<{ [key: string]: any }[]>;
rowFilterReducer: (
items: { [key: string]: any }[],
filters: string | string[],
) => { [key: string]: any }[];
textFilterReducer: (items: { [key: string]: any }[], filters: string) => { [key: string]: any }[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DetailsPage } from '@console/internal/components/factory';
import { K8sResourceKindReference } from '@console/internal/module/k8s';
import HelmReleaseResources from './HelmReleaseResources';
import HelmReleaseOverview from './HelmReleaseOverview';
import HelmReleaseHistory from './HelmReleaseHistory';
import { deleteHelmRelease } from '../../actions/modify-helm-release';

const SecretReference: K8sResourceKindReference = 'Secret';
Expand Down Expand Up @@ -70,6 +71,11 @@ const HelmReleaseDetailsPage: React.FC<HelmReleaseDetailsPageProps> = ({ secret,
name: 'Resources',
component: HelmReleaseResources,
},
{
href: 'history',
name: 'History',
component: HelmReleaseHistory,
},
]}
customKind={HelmReleaseReference}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import { match as RMatch } from 'react-router';
import { coFetchJSON } from '@console/internal/co-fetch';
import { SortByDirection } from '@patternfly/react-table';
import CustomResourceList from '../custom-resource-list/CustomResourceList';
import { helmRowFilters, getFilteredItemsByRow, getFilteredItemsByText } from './helm-utils';
import HelmReleaseHistoryRow from './HelmReleaseHistoryRow';
import HelmReleaseHistoryHeader from './HelmReleaseHistoryHeader';
import { HelmRelease } from './helm-types';

interface HelmReleaseHistoryProps {
match: RMatch<{
ns?: string;
name?: string;
}>;
}

const HelmReleaseHistory: React.FC<HelmReleaseHistoryProps> = ({ match }) => {
const namespace = match.params.ns;
const helmReleaseName = match.params.name;

const getHelmReleaseRevisions = (): Promise<HelmRelease[]> => {
return coFetchJSON(`/api/helm/release/history?ns=${namespace}&name=${helmReleaseName}`);
};

return (
<CustomResourceList
fetchCustomResources={getHelmReleaseRevisions}
queryArg="rowFilter-helm-release-status"
rowFilters={helmRowFilters}
sortBy="version"
sortOrder={SortByDirection.desc}
rowFilterReducer={getFilteredItemsByRow}
textFilterReducer={getFilteredItemsByText}
resourceRow={HelmReleaseHistoryRow}
resourceHeader={HelmReleaseHistoryHeader}
/>
);
};

export default HelmReleaseHistory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { sortable } from '@patternfly/react-table';

export const tableColumnClasses = {
revision: 'col-lg-1 col-md-3 col-sm-3 col-xs-3',
updated: 'col-lg-2 col-md-3 col-sm-5 col-xs-5',
status: 'col-lg-1 col-md-2 hidden-sm hidden-xs',
chartName: 'col-lg-2 hidden-md hidden-sm hidden-xs',
chartVersion: 'col-lg-2 hidden-md hidden-sm hidden-xs',
appVersion: 'col-lg-2 hidden-md hidden-sm hidden-xs',
description: 'col-lg-2 hidden-md hidden-sm hidden-xs',
};

const HelmReleaseHistoryHeader = () => {
return [
{
title: 'Revision',
sortField: 'version',
transforms: [sortable],
props: { className: tableColumnClasses.revision },
},
{
title: 'Updated',
sortField: 'info.last_deployed',
transforms: [sortable],
props: { className: tableColumnClasses.updated },
},
{
title: 'Status',
sortField: 'info.status',
transforms: [sortable],
props: { className: tableColumnClasses.status },
},
{
title: 'Chart Name',
sortField: 'chart.metadata.name',
transforms: [sortable],
props: { className: tableColumnClasses.chartName },
},
{
title: 'Chart Version',
sortField: 'chart.metadata.version',
transforms: [sortable],
props: { className: tableColumnClasses.chartVersion },
},
{
title: 'App Version',
sortField: 'chart.metadata.appVersion',
transforms: [sortable],
props: { className: tableColumnClasses.appVersion },
},
{
title: 'Description',
props: { className: tableColumnClasses.description },
},
];
};

export default HelmReleaseHistoryHeader;