fix: pipeline change history: update config deployment status without having to reload (#3284)

* fix: specify rowKey for pipeline ChangeHistory table

* fix: pipeline change history: refetch pipelines until latest config deployment is finished

* fix: typo: verison -> version

* chore: some code clean up

* fix: get tests passing

* fix: use refetchInterval to specify pipeline data polling strategy

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Raj Kamal Singh 2023-08-08 17:55:09 +05:30 committed by GitHub
parent 3d03ad52b1
commit 7ad489ebb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 52 additions and 32 deletions

View File

@ -5,12 +5,13 @@ import { changeHistoryColumns } from '../../PipelineListsView/config';
import { HistoryTableWrapper } from '../../styles'; import { HistoryTableWrapper } from '../../styles';
import { historyPagination } from '../config'; import { historyPagination } from '../config';
function ChangeHistory({ piplineData }: ChangeHistoryProps): JSX.Element { function ChangeHistory({ pipelineData }: ChangeHistoryProps): JSX.Element {
return ( return (
<HistoryTableWrapper> <HistoryTableWrapper>
<Table <Table
columns={changeHistoryColumns} columns={changeHistoryColumns}
dataSource={piplineData?.history ?? []} dataSource={pipelineData?.history ?? []}
rowKey="id"
pagination={historyPagination} pagination={historyPagination}
/> />
</HistoryTableWrapper> </HistoryTableWrapper>
@ -18,7 +19,7 @@ function ChangeHistory({ piplineData }: ChangeHistoryProps): JSX.Element {
} }
interface ChangeHistoryProps { interface ChangeHistoryProps {
piplineData: Pipeline; pipelineData: Pipeline;
} }
export default ChangeHistory; export default ChangeHistory;

View File

@ -11,13 +11,13 @@ function CreatePipelineButton({
setActionType, setActionType,
isActionMode, isActionMode,
setActionMode, setActionMode,
piplineData, pipelineData,
}: CreatePipelineButtonProps): JSX.Element { }: CreatePipelineButtonProps): JSX.Element {
const { t } = useTranslation(['pipeline']); const { t } = useTranslation(['pipeline']);
const isAddNewPipelineVisible = useMemo( const isAddNewPipelineVisible = useMemo(
() => checkDataLength(piplineData?.pipelines), () => checkDataLength(pipelineData?.pipelines),
[piplineData?.pipelines], [pipelineData?.pipelines],
); );
const isDisabled = isActionMode === ActionMode.Editing; const isDisabled = isActionMode === ActionMode.Editing;
@ -56,7 +56,7 @@ interface CreatePipelineButtonProps {
setActionType: (actionType: string) => void; setActionType: (actionType: string) => void;
isActionMode: string; isActionMode: string;
setActionMode: (actionMode: string) => void; setActionMode: (actionMode: string) => void;
piplineData: Pipeline; pipelineData: Pipeline;
} }
export default CreatePipelineButton; export default CreatePipelineButton;

View File

@ -7,7 +7,7 @@ import PipelinesSearchSection from './PipelinesSearchSection';
function PipelinePageLayout({ function PipelinePageLayout({
refetchPipelineLists, refetchPipelineLists,
piplineData, pipelineData,
}: PipelinePageLayoutProps): JSX.Element { }: PipelinePageLayoutProps): JSX.Element {
const [isActionType, setActionType] = useState<string>(); const [isActionType, setActionType] = useState<string>();
const [isActionMode, setActionMode] = useState<string>('viewing-mode'); const [isActionMode, setActionMode] = useState<string>('viewing-mode');
@ -19,7 +19,7 @@ function PipelinePageLayout({
setActionType={setActionType} setActionType={setActionType}
setActionMode={setActionMode} setActionMode={setActionMode}
isActionMode={isActionMode} isActionMode={isActionMode}
piplineData={piplineData} pipelineData={pipelineData}
/> />
<PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} /> <PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} />
<PipelineListsView <PipelineListsView
@ -27,7 +27,7 @@ function PipelinePageLayout({
setActionType={setActionType} setActionType={setActionType}
setActionMode={setActionMode} setActionMode={setActionMode}
isActionMode={isActionMode} isActionMode={isActionMode}
piplineData={piplineData} pipelineData={pipelineData}
refetchPipelineLists={refetchPipelineLists} refetchPipelineLists={refetchPipelineLists}
pipelineSearchValue={pipelineSearchValue} pipelineSearchValue={pipelineSearchValue}
/> />
@ -37,7 +37,7 @@ function PipelinePageLayout({
interface PipelinePageLayoutProps { interface PipelinePageLayoutProps {
refetchPipelineLists: VoidFunction; refetchPipelineLists: VoidFunction;
piplineData: Pipeline; pipelineData: Pipeline;
} }
export default PipelinePageLayout; export default PipelinePageLayout;

View File

@ -4,21 +4,21 @@ import { ModeAndConfigWrapper } from './styles';
function ModeAndConfiguration({ function ModeAndConfiguration({
isActionMode, isActionMode,
verison, version,
}: ModeAndConfigurationType): JSX.Element { }: ModeAndConfigurationType): JSX.Element {
const actionMode = isActionMode === ActionMode.Editing; const actionMode = isActionMode === ActionMode.Editing;
return ( return (
<ModeAndConfigWrapper> <ModeAndConfigWrapper>
Mode: <span>{actionMode ? 'Editing' : 'Viewing'}</span> Mode: <span>{actionMode ? 'Editing' : 'Viewing'}</span>
<div>Configuration Version: {verison}</div> <div>Configuration Version: {version}</div>
</ModeAndConfigWrapper> </ModeAndConfigWrapper>
); );
} }
export interface ModeAndConfigurationType { export interface ModeAndConfigurationType {
isActionMode: string; isActionMode: string;
verison: string | number; version: string | number;
} }
export default ModeAndConfiguration; export default ModeAndConfiguration;

View File

@ -47,7 +47,7 @@ function PipelineListsView({
setActionType, setActionType,
isActionMode, isActionMode,
setActionMode, setActionMode,
piplineData, pipelineData,
refetchPipelineLists, refetchPipelineLists,
pipelineSearchValue, pipelineSearchValue,
}: PipelineListsViewProps): JSX.Element { }: PipelineListsViewProps): JSX.Element {
@ -55,10 +55,10 @@ function PipelineListsView({
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>( const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(piplineData?.pipelines), cloneDeep(pipelineData?.pipelines),
); );
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>( const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
cloneDeep(piplineData?.pipelines), cloneDeep(pipelineData?.pipelines),
); );
const [ const [
expandedPipelineData, expandedPipelineData,
@ -77,14 +77,14 @@ function PipelineListsView({
const isEditingActionMode = isActionMode === ActionMode.Editing; const isEditingActionMode = isActionMode === ActionMode.Editing;
useEffect(() => { useEffect(() => {
if (pipelineSearchValue === '') setCurrPipelineData(piplineData?.pipelines); if (pipelineSearchValue === '') setCurrPipelineData(pipelineData?.pipelines);
if (pipelineSearchValue !== '') { if (pipelineSearchValue !== '') {
const filterData = piplineData?.pipelines.filter((data: PipelineData) => const filterData = pipelineData?.pipelines.filter((data: PipelineData) =>
getDataOnSearch(data as never, pipelineSearchValue), getDataOnSearch(data as never, pipelineSearchValue),
); );
setCurrPipelineData(filterData); setCurrPipelineData(filterData);
} }
}, [pipelineSearchValue, piplineData?.pipelines]); }, [pipelineSearchValue, pipelineData?.pipelines]);
const handleAlert = useCallback( const handleAlert = useCallback(
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => { ({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
@ -414,7 +414,7 @@ function PipelineListsView({
<Container> <Container>
<ModeAndConfiguration <ModeAndConfiguration
isActionMode={isActionMode} isActionMode={isActionMode}
verison={piplineData?.version} version={pipelineData?.version}
/> />
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
@ -445,7 +445,7 @@ interface PipelineListsViewProps {
setActionType: (actionType?: ActionType) => void; setActionType: (actionType?: ActionType) => void;
isActionMode: string; isActionMode: string;
setActionMode: (actionMode: ActionMode) => void; setActionMode: (actionMode: ActionMode) => void;
piplineData: Pipeline; pipelineData: Pipeline;
refetchPipelineLists: VoidFunction; refetchPipelineLists: VoidFunction;
pipelineSearchValue: string; pipelineSearchValue: string;
} }

View File

@ -1,6 +1,6 @@
import { Pipeline, PipelineData } from 'types/api/pipeline/def'; import { Pipeline, PipelineData } from 'types/api/pipeline/def';
export const configurationVerison = '1.0'; export const configurationVersion = '1.0';
export const pipelineMockData: Array<PipelineData> = [ export const pipelineMockData: Array<PipelineData> = [
{ {

View File

@ -18,7 +18,7 @@ describe('PipelinePage container test', () => {
setActionType={jest.fn()} setActionType={jest.fn()}
isActionMode="viewing-mode" isActionMode="viewing-mode"
setActionMode={jest.fn()} setActionMode={jest.fn()}
piplineData={pipelineApiResponseMockData} pipelineData={pipelineApiResponseMockData}
/> />
</I18nextProvider> </I18nextProvider>
</Provider> </Provider>

View File

@ -49,7 +49,7 @@ describe('PipelinePage container test', () => {
<Provider store={store}> <Provider store={store}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<PipelinePageLayout <PipelinePageLayout
piplineData={pipelinedata} pipelineData={pipelinedata}
refetchPipelineLists={refetchPipelineLists} refetchPipelineLists={refetchPipelineLists}
/> />
</I18nextProvider> </I18nextProvider>

View File

@ -8,14 +8,30 @@ import { useNotifications } from 'hooks/useNotifications';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def'; import { Pipeline } from 'types/api/pipeline/def';
const pipelineRefetchInterval = (
pipelineResponse: SuccessResponse<Pipeline> | undefined,
): number | false => {
// Refetch pipeline data periodically if deployment of
// its latest changes is not complete yet.
const latestVersion = pipelineResponse?.payload?.history?.[0];
const isLatestDeploymentFinished = ['DEPLOYED', 'FAILED'].includes(
latestVersion?.deployStatus || '',
);
if (latestVersion && !isLatestDeploymentFinished) {
return 3000;
}
return false;
};
function Pipelines(): JSX.Element { function Pipelines(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const { const {
isLoading, isLoading,
data: piplineData, data: pipelineData,
isError, isError,
refetch: refetchPipelineLists, refetch: refetchPipelineLists,
} = useQuery(['version', 'latest', 'pipeline'], { } = useQuery(['version', 'latest', 'pipeline'], {
@ -23,6 +39,7 @@ function Pipelines(): JSX.Element {
getPipeline({ getPipeline({
version: 'latest', version: 'latest',
}), }),
refetchInterval: pipelineRefetchInterval,
}); });
const tabItems: TabsProps['items'] = useMemo( const tabItems: TabsProps['items'] = useMemo(
@ -33,26 +50,28 @@ function Pipelines(): JSX.Element {
children: ( children: (
<PipelinePage <PipelinePage
refetchPipelineLists={refetchPipelineLists} refetchPipelineLists={refetchPipelineLists}
piplineData={piplineData?.payload as Pipeline} pipelineData={pipelineData?.payload as Pipeline}
/> />
), ),
}, },
{ {
key: 'change-history', key: 'change-history',
label: `Change History`, label: `Change History`,
children: <ChangeHistory piplineData={piplineData?.payload as Pipeline} />, children: (
<ChangeHistory pipelineData={pipelineData?.payload as Pipeline} />
),
}, },
], ],
[piplineData?.payload, refetchPipelineLists], [pipelineData?.payload, refetchPipelineLists],
); );
useEffect(() => { useEffect(() => {
if (piplineData?.error && isError) { if (pipelineData?.error && isError) {
notifications.error({ notifications.error({
message: piplineData?.error || t('something_went_wrong'), message: pipelineData?.error || t('something_went_wrong'),
}); });
} }
}, [isError, notifications, piplineData?.error, t]); }, [isError, notifications, pipelineData?.error, t]);
if (isLoading) { if (isLoading) {
return <Spinner height="75vh" tip="Loading Pipelines..." />; return <Spinner height="75vh" tip="Loading Pipelines..." />;