From 73d382b4d06643abfb451547f092011559b2076c Mon Sep 17 00:00:00 2001 From: GnsP Date: Mon, 20 Nov 2023 21:44:15 +0530 Subject: [PATCH 1/3] ux improvements --- app/cdap/api/longRunningOperation.ts | 4 +- app/cdap/components/FooterContext/index.ts | 41 +++++ .../SourceControlManagementForm.tsx | 24 +-- .../SourceControlManagement/index.tsx | 2 +- .../PullPipelineWizard.tsx | 2 +- .../LocalPipelineListView/PipelineTable.tsx | 144 ++++++++++++++---- .../LocalPipelineListView/index.tsx | 99 +++++++----- .../OperationAlert.tsx | 22 ++- .../OperationsHistoryView/index.tsx | 47 ++++++ .../RemotePipelineTable.tsx | 126 ++++++++++++--- .../RemotePipelineListView/index.tsx | 105 ++++++++----- .../SourceControlManagement/SearchBox.tsx | 2 +- .../SyncStatusFilters.tsx | 76 +++++++++ .../SourceControlManagement/SyncTabs.tsx | 57 +++++-- .../SourceControlManagement/helpers.ts | 70 ++++++++- .../SourceControlManagement/index.tsx | 10 +- .../store/ActionCreator.ts | 124 ++++++++++++++- .../SourceControlManagement/store/index.ts | 39 ++++- .../SourceControlManagement/styles.ts | 17 ++- .../SourceControlManagement/types.ts | 4 + app/cdap/main.js | 31 +++- app/cdap/styles/common.scss | 4 + app/cdap/text/text-en.yaml | 12 ++ package.json | 83 +++++----- yarn.lock | 5 + 25 files changed, 945 insertions(+), 205 deletions(-) create mode 100644 app/cdap/components/FooterContext/index.ts create mode 100644 app/cdap/components/SourceControlManagement/OperationsHistoryView/index.tsx create mode 100644 app/cdap/components/SourceControlManagement/SyncStatusFilters.tsx diff --git a/app/cdap/api/longRunningOperation.ts b/app/cdap/api/longRunningOperation.ts index 63e57137ef7..9c8618144eb 100644 --- a/app/cdap/api/longRunningOperation.ts +++ b/app/cdap/api/longRunningOperation.ts @@ -27,13 +27,13 @@ export const LongRunningOperationApi = { dataSrc, 'GET', 'REQUEST', - `${basePath}/?pageSize=1&filter=${PUSH_FILTER}` + `${basePath}/?pageSize=10&filter=${PUSH_FILTER}` ), getLatestPull: apiCreator( dataSrc, 'GET', 'REQUEST', - `${basePath}/?pageSize=1&filter=${PULL_FILTER}` + `${basePath}/?pageSize=10&filter=${PULL_FILTER}` ), pollOperation: apiCreator(dataSrc, 'GET', 'POLL', `${basePath}/:operationId`), stopOperation: apiCreator(dataSrc, 'POST', 'REQUEST', `${basePath}/:operationId/stop`), diff --git a/app/cdap/components/FooterContext/index.ts b/app/cdap/components/FooterContext/index.ts new file mode 100644 index 00000000000..a71cc8cb24a --- /dev/null +++ b/app/cdap/components/FooterContext/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import { createContext, useContext, useEffect } from 'react'; + +interface IFooterContext { + show: boolean; + setShow(val?: boolean): void; +} + +export const FooterContext = createContext({ + show: true, + setShow() { + return; + }, +}); + +export function useHideFooterInPage() { + const { setShow } = useContext(FooterContext); + + useEffect(() => { + setShow(false); + + return () => setShow(true); + }, []); + + return setShow; +} diff --git a/app/cdap/components/NamespaceAdmin/SourceControlManagement/SourceControlManagementForm.tsx b/app/cdap/components/NamespaceAdmin/SourceControlManagement/SourceControlManagementForm.tsx index 795a416c8f4..9d8e8792456 100644 --- a/app/cdap/components/NamespaceAdmin/SourceControlManagement/SourceControlManagementForm.tsx +++ b/app/cdap/components/NamespaceAdmin/SourceControlManagement/SourceControlManagementForm.tsx @@ -340,17 +340,19 @@ const SourceControlManagementForm = ({ : [] } /> - { - handleValueChange(val, 'username'); - }} - /> + {formState.config?.provider !== providers.github && ( + { + handleValueChange(val, 'username'); + }} + /> + )} )} diff --git a/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx b/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx index 938051ae986..3601a85e3f8 100644 --- a/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx +++ b/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx @@ -27,7 +27,6 @@ import TableBody from 'components/shared/Table/TableBody'; import { useSelector } from 'react-redux'; import ActionsPopover from 'components/shared/ActionsPopover'; import { UnlinkSourceControlModal } from './UnlinkSourceControlModal'; -import StyledPasswordWrapper from 'components/AbstractWidget/FormInputs/Password'; import { ISourceControlManagementConfig } from './types'; import SourceControlManagementForm from './SourceControlManagementForm'; import PrimaryTextButton from 'components/shared/Buttons/PrimaryTextButton'; @@ -58,6 +57,7 @@ export const SourceControlManagement = () => { const sourceControlManagementConfig: ISourceControlManagementConfig = useSelector( (state) => state.sourceControlManagementConfig ); + const toggleForm = () => { setIsFormOpen(!isFormOpen); }; diff --git a/app/cdap/components/ResourceCenterEntity/PullPipelineWizard.tsx b/app/cdap/components/ResourceCenterEntity/PullPipelineWizard.tsx index 390d3c99184..0a13fabbc27 100644 --- a/app/cdap/components/ResourceCenterEntity/PullPipelineWizard.tsx +++ b/app/cdap/components/ResourceCenterEntity/PullPipelineWizard.tsx @@ -80,7 +80,7 @@ export const PullPipelineWizard = ({ isOpen, error, dispatch }: IPullPipelineWiz toggle={() => dispatch({ type: 'TOGGLE_MODAL' })} > - + diff --git a/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx b/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx index a72dd70ec37..17e416a5766 100644 --- a/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx +++ b/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx @@ -14,11 +14,22 @@ * the License. */ -import React from 'react'; -import { Checkbox, Table, TableBody, TableCell, TableRow, TableHead } from '@material-ui/core'; +import React, { useState } from 'react'; +import { + Checkbox, + Table, + TableBody, + TableCell, + TableRow, + TableHead, + TableSortLabel, + TablePagination, +} from '@material-ui/core'; import InfoIcon from '@material-ui/icons/Info'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import ErrorIcon from '@material-ui/icons/Error'; import { setSelectedPipelines } from '../store/ActionCreator'; -import { IRepositoryPipeline } from '../types'; +import { IRepositoryPipeline, TSyncStatusFilter } from '../types'; import T from 'i18n-react'; import StatusButton from 'components/StatusButton'; import { SUPPORT } from 'components/StatusButton/constants'; @@ -29,8 +40,12 @@ import { StatusCell, StyledFixedWidthCell, StyledPopover, + SyncStatusWrapper, } from '../styles'; import { timeInstantToString } from 'services/DataFormatter'; +import { compareSyncStatus, filterOnSyncStatus, stableSort } from '../helpers'; +import LoadingSVG from 'components/shared/LoadingSVG'; +import { green, red } from '@material-ui/core/colors'; const PREFIX = 'features.SourceControlManagement.table'; @@ -38,18 +53,42 @@ interface IRepositoryPipelineTableProps { localPipelines: IRepositoryPipeline[]; selectedPipelines: string[]; showFailedOnly: boolean; - enableMultipleSelection?: boolean; + multiPushEnabled?: boolean; disabled?: boolean; + syncStatusFilter?: TSyncStatusFilter; + lastOperationInfoShown?: boolean; } export const LocalPipelineTable = ({ localPipelines, selectedPipelines, showFailedOnly, - enableMultipleSelection = false, + multiPushEnabled = false, disabled = false, + syncStatusFilter = 'all', + lastOperationInfoShown = true, }: IRepositoryPipelineTableProps) => { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const isSelected = (name: string) => selectedPipelines.indexOf(name) !== -1; + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const syncStatusComparator = (a: IRepositoryPipeline, b: IRepositoryPipeline) => { + return sortOrder === 'desc' ? compareSyncStatus(a, b) : -compareSyncStatus(a, b); + }; + + const filteredPipelines = filterOnSyncStatus(localPipelines, syncStatusFilter); + const displayedPipelines = stableSort(filteredPipelines, syncStatusComparator).slice( + page * rowsPerPage, + (page + 1) * rowsPerPage + ); + const displayedPipelineNames = displayedPipelines.map((pipeline) => pipeline.name); + + const selectedPipelinesSet = new Set(selectedPipelines); + const isAllDisplayedPipelinesSelected = displayedPipelineNames.reduce((acc, pipelineName) => { + return acc && selectedPipelinesSet.has(pipelineName); + }, true); const handleSelectAllClick = (event: React.ChangeEvent) => { if (disabled) { @@ -57,8 +96,7 @@ export const LocalPipelineTable = ({ } if (event.target.checked) { - const allSelected = localPipelines.map((pipeline) => pipeline.name); - setSelectedPipelines(allSelected); + setSelectedPipelines(displayedPipelineNames); return; } setSelectedPipelines([]); @@ -69,7 +107,7 @@ export const LocalPipelineTable = ({ return; } - if (enableMultipleSelection) { + if (multiPushEnabled) { handleMultipleSelection(name); return; } @@ -98,19 +136,31 @@ export const LocalPipelineTable = ({ setSelectedPipelines(newSelected); }; + const handleSort = () => { + const isAsc = sortOrder === 'asc'; + setSortOrder(isAsc ? 'desc' : 'asc'); + }; + + const handleChangePage = (event, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + return ( - + - {enableMultipleSelection && ( + {multiPushEnabled && ( 0 && selectedPipelines.length < localPipelines.length - } - checked={selectedPipelines.length === localPipelines.length} + indeterminate={selectedPipelines.length > 0 && !isAllDisplayedPipelinesSelected} + checked={isAllDisplayedPipelinesSelected} onChange={handleSelectAllClick} disabled={disabled} /> @@ -119,18 +169,27 @@ export const LocalPipelineTable = ({ {T.translate(`${PREFIX}.pipelineName`)} {T.translate(`${PREFIX}.lastSyncDate`)} - -
- {T.translate(`${PREFIX}.gitStatus`)} - } showOn="Hover"> - {T.translate(`${PREFIX}.gitStatusHelperText`)} - -
-
+ {multiPushEnabled && ( + + + {T.translate(`${PREFIX}.gitSyncStatus`)} + + + )} + {!multiPushEnabled && ( + +
+ {T.translate(`${PREFIX}.gitStatus`)} + } showOn="Hover"> + {T.translate(`${PREFIX}.gitStatusHelperText`)} + +
+
+ )}
- {localPipelines.map((pipeline: IRepositoryPipeline) => { + {displayedPipelines.map((pipeline: IRepositoryPipeline) => { if (showFailedOnly && !pipeline.error) { // only render pipelines that failed to push return; @@ -170,14 +229,47 @@ export const LocalPipelineTable = ({ {pipeline.name} {timeInstantToString(pipeline.lastSyncDate)} - - {pipeline.fileHash ? T.translate(`${PREFIX}.connected`) : '--'} - + {multiPushEnabled && ( + + {pipeline.syncStatus === undefined || + pipeline.syncStatus === 'not_available' ? ( + + + {T.translate(`${PREFIX}.gitSyncStatusFetching`)} + + ) : pipeline.syncStatus === 'not_connected' || + pipeline.syncStatus === 'out_of_sync' ? ( + + {' '} + {T.translate(`${PREFIX}.gitSyncStatusUnsynced`)} + + ) : ( + + + {T.translate(`${PREFIX}.gitSyncStatusSynced`)} + + )} + + )} + {!multiPushEnabled && ( + + {pipeline.fileHash ? T.translate(`${PREFIX}.connected`) : '--'} + + )} ); })}
+
); }; diff --git a/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx b/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx index 1e0593b5aab..b743cf608cd 100644 --- a/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx +++ b/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx @@ -15,20 +15,24 @@ */ import PrimaryContainedButton from 'components/shared/Buttons/PrimaryContainedButton'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { getCurrentNamespace } from 'services/NamespaceStore'; import { countPushFailedPipelines, + dismissOperationAlert, fetchLatestOperation, getNamespacePipelineList, pushMultipleSelectedPipelines, pushSelectedPipelines, - reset, + refetchAllPipelines, resetPushStatus, setLoadingMessage, setLocalPipelines, setNameFilter, + setSyncStatusFilter, + setSyncStatusOfAllPipelines, + setSyncStatusOfRemotePipelines, toggleCommitModal, toggleShowFailedOnly, } from '../store/ActionCreator'; @@ -40,11 +44,17 @@ import cloneDeep from 'lodash/cloneDeep'; import PrimaryTextButton from 'components/shared/Buttons/PrimaryTextButton'; import { LocalPipelineTable } from './PipelineTable'; import { useOnUnmount } from 'services/react/customHooks/useOnUnmount'; -import { FailStatusDiv, PipelineListContainer, StyledSelectionStatusDiv } from '../styles'; +import { + FailStatusDiv, + FiltersAndStatusWrapper, + PipelineListContainer, + StyledSelectionStatusDiv, +} from '../styles'; import { IListResponse, IOperationMetaResponse, IOperationRun } from '../types'; import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; import { parseOperationResource } from '../helpers'; import { OperationAlert } from '../OperationAlert'; +import { SyncStatusFilters } from '../SyncStatusFilters'; const PREFIX = 'features.SourceControlManagement.push'; @@ -57,9 +67,10 @@ export const LocalPipelineListView = () => { commitModalOpen, loadingMessage, showFailedOnly, + syncStatusFilter, } = useSelector(({ push }) => push); - const { running: isAnOperationRunning, operation } = useSelector( + const { running: isAnOperationRunning, operation, showLastOperationInfo } = useSelector( ({ operationRun }) => operationRun ); @@ -80,8 +91,6 @@ export const LocalPipelineListView = () => { } }, []); - useOnUnmount(() => reset()); - const onPushSubmit = (commitMessage: string) => { resetPushStatus(); const pushedPipelines = cloneDeep(localPipelines); @@ -108,6 +117,8 @@ export const LocalPipelineListView = () => { }, complete() { setLoadingMessage(null); + refetchAllPipelines(); + setSyncStatusOfAllPipelines(); }, }); @@ -131,6 +142,8 @@ export const LocalPipelineListView = () => { }, complete() { setLoadingMessage(null); + refetchAllPipelines(); + setSyncStatusOfAllPipelines(); }, }); }; @@ -143,8 +156,10 @@ export const LocalPipelineListView = () => { localPipelines={localPipelines} selectedPipelines={selectedPipelines} showFailedOnly={showFailedOnly} - enableMultipleSelection={multiPushEnabled} + multiPushEnabled={multiPushEnabled} disabled={isAnOperationRunning} + syncStatusFilter={syncStatusFilter} + lastOperationInfoShown={showLastOperationInfo} /> { return ( - - {operation && multiPushEnabled && } - {selectedPipelines.length > 0 && ( - -
- {T.translate(`${PREFIX}.pipelinesSelected`, { - selected: selectedPipelines.length, - total: localPipelines.length, - })} -
- {!multiPushEnabled && pushFailedCount > 0 && ( - <> - - {pushFailedCount === 1 - ? T.translate(`${PREFIX}.pipelinePushedFail`) - : T.translate(`${PREFIX}.pipelinesPushedFail`, { - count: pushFailedCount.toString(), - })} - - - {showFailedOnly - ? T.translate('commons.showAll') - : T.translate('commons.showFailed')} - - - )} - {multiPushEnabled && pushFailedCount > 0 && ( - {T.translate(`${PREFIX}.pipelinesPushedFailMulti`)} - )} -
+ {operation && multiPushEnabled && showLastOperationInfo && ( + )} + + + {selectedPipelines.length > 0 && ( + +
+ {T.translate(`${PREFIX}.pipelinesSelected`, { + selected: selectedPipelines.length, + total: localPipelines.length, + })} +
+ {!multiPushEnabled && pushFailedCount > 0 && ( + <> + + {pushFailedCount === 1 + ? T.translate(`${PREFIX}.pipelinePushedFail`) + : T.translate(`${PREFIX}.pipelinesPushedFail`, { + count: pushFailedCount.toString(), + })} + + + {showFailedOnly + ? T.translate('commons.showAll') + : T.translate('commons.showFailed')} + + + )} + {multiPushEnabled && pushFailedCount > 0 && ( + {T.translate(`${PREFIX}.pipelinesPushedFailMulti`)} + )} +
+ )} + {multiPushEnabled && ( + + )} +
{ready ? LocalPipelineTableComp() : } { +export const OperationAlert = ({ operation, onClose }: IOperationBannerProps) => { const [viewErrorExpanded, setViewErrorExpanded] = useState(false); const getOperationAction = () => { @@ -87,11 +92,22 @@ export const OperationAlert = ({ operation }: IOperationBannerProps) => { > {viewErrorExpanded ? : } + {onClose && ( + + )} ); } - return undefined; + return ( + onClose && ( + + ) + ); }; const renderOperationTime = () => { diff --git a/app/cdap/components/SourceControlManagement/OperationsHistoryView/index.tsx b/app/cdap/components/SourceControlManagement/OperationsHistoryView/index.tsx new file mode 100644 index 00000000000..2a1024f00f9 --- /dev/null +++ b/app/cdap/components/SourceControlManagement/OperationsHistoryView/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { OperationAlert } from '../OperationAlert'; +import { fetchLatestOperation } from '../store/ActionCreator'; +import { getCurrentNamespace } from 'services/NamespaceStore'; + +const OperationsHistoryContainer = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + + padding: 16px; + gap: 8px; +`; + +export function OperationsHistoryView() { + const { allOperations } = useSelector(({ operationRun }) => operationRun); + + useEffect(() => { + fetchLatestOperation(getCurrentNamespace()); + }, []); + + return ( + + {allOperations.map((op) => ( + + ))} + + ); +} diff --git a/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx b/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx index dca38c86905..31db1ec13c4 100644 --- a/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx +++ b/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx @@ -14,14 +14,34 @@ * the License. */ -import React from 'react'; +import React, { useState } from 'react'; import T from 'i18n-react'; -import { Checkbox, Table, TableBody, TableCell, TableRow, TableHead } from '@material-ui/core'; +import { + Checkbox, + Table, + TableBody, + TableCell, + TableRow, + TableHead, + TableSortLabel, + TablePagination, +} from '@material-ui/core'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import ErrorIcon from '@material-ui/icons/Error'; import { setSelectedRemotePipelines } from '../store/ActionCreator'; -import { IRepositoryPipeline } from '../types'; +import { IRepositoryPipeline, TSyncStatusFilter } from '../types'; import StatusButton from 'components/StatusButton'; import { SUPPORT } from 'components/StatusButton/constants'; -import { StyledTableCell, StyledTableRow, TableBox } from '../styles'; +import { + StyledFixedWidthCell, + StyledTableCell, + StyledTableRow, + SyncStatusWrapper, + TableBox, +} from '../styles'; +import { compareSyncStatus, filterOnSyncStatus, stableSort } from '../helpers'; +import LoadingSVG from 'components/shared/LoadingSVG'; +import { green, red } from '@material-ui/core/colors'; const PREFIX = 'features.SourceControlManagement.table'; @@ -29,25 +49,49 @@ interface IRepositoryPipelineTableProps { remotePipelines: IRepositoryPipeline[]; selectedPipelines: string[]; showFailedOnly: boolean; - enableMultipleSelection?: boolean; + multiPullEnabled?: boolean; disabled?: boolean; + syncStatusFilter?: TSyncStatusFilter; + lastOperationInfoShown?: boolean; } export const RemotePipelineTable = ({ remotePipelines, selectedPipelines, showFailedOnly, - enableMultipleSelection = false, + multiPullEnabled = false, disabled = false, + syncStatusFilter = 'all', + lastOperationInfoShown = true, }: IRepositoryPipelineTableProps) => { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const isSelected = (name: string) => selectedPipelines.indexOf(name) !== -1; + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const syncStatusComparator = (a: IRepositoryPipeline, b: IRepositoryPipeline) => { + return sortOrder === 'desc' ? compareSyncStatus(a, b) : -compareSyncStatus(a, b); + }; + + const filteredPipelines = filterOnSyncStatus(remotePipelines, syncStatusFilter); + const displayedPipelines = stableSort(filteredPipelines, syncStatusComparator).slice( + page * rowsPerPage, + (page + 1) * rowsPerPage + ); + const displayedPipelineNames = displayedPipelines.map((pipeline) => pipeline.name); + + const selectedPipelinesSet = new Set(selectedPipelines); + const isAllDisplayedPipelinesSelected = displayedPipelineNames.reduce((acc, pipelineName) => { + return acc && selectedPipelinesSet.has(pipelineName); + }, true); const handleClick = (event: React.MouseEvent, name: string) => { if (disabled) { return; } - if (enableMultipleSelection) { + if (multiPullEnabled) { handleMultipleSelection(name); return; } @@ -83,27 +127,37 @@ export const RemotePipelineTable = ({ } if (event.target.checked) { - const allSelected = remotePipelines.map((pipeline) => pipeline.name); - setSelectedRemotePipelines(allSelected); + setSelectedRemotePipelines(displayedPipelineNames); return; } setSelectedRemotePipelines([]); }; + const handleSort = () => { + const isAsc = sortOrder === 'asc'; + setSortOrder(isAsc ? 'desc' : 'asc'); + }; + + const handleChangePage = (event, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + return ( - + - {enableMultipleSelection && ( + {multiPullEnabled && ( 0 && - selectedPipelines.length < remotePipelines.length - } - checked={selectedPipelines.length === remotePipelines.length} + indeterminate={selectedPipelines.length > 0 && !isAllDisplayedPipelinesSelected} + checked={isAllDisplayedPipelinesSelected} onChange={handleSelectAllClick} disabled={disabled} /> @@ -111,10 +165,17 @@ export const RemotePipelineTable = ({ {T.translate(`${PREFIX}.pipelineName`)} + {multiPullEnabled && ( + + + {T.translate(`${PREFIX}.gitSyncStatus`)} + + + )} - {remotePipelines.map((pipeline: IRepositoryPipeline) => { + {displayedPipelines.map((pipeline: IRepositoryPipeline) => { if (showFailedOnly && !pipeline.error) { // only render pipelines that failed to pull return; @@ -154,11 +215,42 @@ export const RemotePipelineTable = ({ )} {pipeline.name} + {multiPullEnabled && ( + + {pipeline.syncStatus === undefined || + pipeline.syncStatus === 'not_available' ? ( + + + {T.translate(`${PREFIX}.gitSyncStatusFetching`)} + + ) : pipeline.syncStatus === 'not_connected' || + pipeline.syncStatus === 'out_of_sync' ? ( + + {' '} + {T.translate(`${PREFIX}.gitSyncStatusUnsynced`)} + + ) : ( + + + {T.translate(`${PREFIX}.gitSyncStatusSynced`)} + + )} + + )} ); })}
+
); }; diff --git a/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx b/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx index bbeeadfbdda..54ea28930ba 100644 --- a/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx +++ b/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx @@ -14,12 +14,17 @@ * the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import T from 'i18n-react'; import { useSelector } from 'react-redux'; import cloneDeep from 'lodash/cloneDeep'; import { SearchBox } from '../SearchBox'; -import { FailStatusDiv, PipelineListContainer, StyledSelectionStatusDiv } from '../styles'; +import { + FailStatusDiv, + FiltersAndStatusWrapper, + PipelineListContainer, + StyledSelectionStatusDiv, +} from '../styles'; import { RemotePipelineTable } from './RemotePipelineTable'; import { countPullFailedPipelines, @@ -27,20 +32,22 @@ import { pullAndDeploySelectedRemotePipelines, resetPullStatus, setPullViewErrorMsg, - resetRemote, setRemoteLoadingMessage, setRemoteNameFilter, setRemotePipelines, toggleRemoteShowFailedOnly, pullAndDeployMultipleSelectedRemotePipelines, fetchLatestOperation, + setRemoteSyncStatusFilter, + setSyncStatusOfAllPipelines, + refetchAllPipelines, + dismissOperationAlert, } from '../store/ActionCreator'; import { LoadingAppLevel } from 'components/shared/LoadingAppLevel'; import { getCurrentNamespace } from 'services/NamespaceStore'; import LoadingSVGCentered from 'components/shared/LoadingSVGCentered'; import PrimaryTextButton from 'components/shared/Buttons/PrimaryTextButton'; import PrimaryContainedButton from 'components/shared/Buttons/PrimaryContainedButton'; -import { useOnUnmount } from 'services/react/customHooks/useOnUnmount'; import { getHydratorUrl } from 'services/UiUtils/UrlGenerator'; import { SUPPORT } from 'components/StatusButton/constants'; import { IListResponse, IOperationMetaResponse, IOperationRun } from '../types'; @@ -48,14 +55,19 @@ import Alert from 'components/shared/Alert'; import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; import { parseOperationResource } from '../helpers'; import { OperationAlert } from '../OperationAlert'; +import { SyncStatusFilters } from '../SyncStatusFilters'; const PREFIX = 'features.SourceControlManagement.pull'; interface IRemotePipelineListViewProps { redirectOnSubmit?: boolean; + singlePipelineMode?: boolean; } -export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineListViewProps) => { +export const RemotePipelineListView = ({ + redirectOnSubmit, + singlePipelineMode, +}: IRemotePipelineListViewProps) => { const { ready, remotePipelines, @@ -64,15 +76,16 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList loadingMessage, showFailedOnly, pullViewErrorMsg, + syncStatusFilter, } = useSelector(({ pull }) => pull); - const { running: isAnOperationRunning, operation } = useSelector( + const { running: isAnOperationRunning, operation, showLastOperationInfo } = useSelector( ({ operationRun }) => operationRun ); - const multiPullEnabled = useFeatureFlagDefaultFalse( - 'source.control.management.multi.app.enabled' - ); + const multiPullEnabled = + useFeatureFlagDefaultFalse('source.control.management.multi.app.enabled') && + !singlePipelineMode; const pullFailedCount = countPullFailedPipelines(); useEffect(() => { @@ -87,8 +100,6 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList } }, []); - useOnUnmount(() => resetRemote()); - const filteredPipelines = remotePipelines.filter((pipeline) => pipeline.name.toLowerCase().includes(nameFilter.toLowerCase()) ); @@ -115,6 +126,8 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList }, complete() { setRemoteLoadingMessage(null); + refetchAllPipelines(); + setSyncStatusOfAllPipelines(); }, }); @@ -144,6 +157,8 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList }, complete() { setRemoteLoadingMessage(null); + refetchAllPipelines(); + setSyncStatusOfAllPipelines(); }, }); }; @@ -156,8 +171,10 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList remotePipelines={filteredPipelines} selectedPipelines={selectedPipelines} showFailedOnly={showFailedOnly} - enableMultipleSelection={multiPullEnabled} + multiPullEnabled={multiPullEnabled} disabled={isAnOperationRunning} + syncStatusFilter={syncStatusFilter} + lastOperationInfoShown={showLastOperationInfo} /> setPullViewErrorMsg()} /> - - {operation && multiPullEnabled && } - {selectedPipelines.length > 0 && ( - -
- {T.translate(`${PREFIX}.pipelinesSelected`, { - selected: selectedPipelines.length, - total: remotePipelines.length, - })} -
- {pullFailedCount > 0 && ( - <> - - {pullFailedCount === 1 - ? T.translate(`${PREFIX}.pipelinePulledFail`) - : T.translate(`${PREFIX}.pipelinesPulledFail`, { - count: pullFailedCount.toString(), - })} - - - {showFailedOnly - ? T.translate('commons.showAll') - : T.translate('commons.showFailed')} - - - )} -
+ {operation && multiPullEnabled && showLastOperationInfo && ( + )} + + + {selectedPipelines.length > 0 && ( + +
+ {T.translate(`${PREFIX}.pipelinesSelected`, { + selected: selectedPipelines.length, + total: remotePipelines.length, + })} +
+ {pullFailedCount > 0 && ( + <> + + {pullFailedCount === 1 + ? T.translate(`${PREFIX}.pipelinePulledFail`) + : T.translate(`${PREFIX}.pipelinesPulledFail`, { + count: pullFailedCount.toString(), + })} + + + {showFailedOnly + ? T.translate('commons.showAll') + : T.translate('commons.showFailed')} + + + )} +
+ )} + {multiPullEnabled && ( + + )} +
{ready ? RemotePipelineTableComp() : }
void; +} + +export const SyncStatusFilters = ({ + syncStatusFilter, + setSyncStatusFilter, +}: ISyncStatusFiltersProps) => { + const setFilter = (filterVal: TSyncStatusFilter) => () => setSyncStatusFilter(filterVal); + + return ( + + {T.translate(`${PREFIX}.filtersLabel`)}: + + + + + + + ); +}; diff --git a/app/cdap/components/SourceControlManagement/SyncTabs.tsx b/app/cdap/components/SourceControlManagement/SyncTabs.tsx index 833f4ba2fc5..d5a17fd3073 100644 --- a/app/cdap/components/SourceControlManagement/SyncTabs.tsx +++ b/app/cdap/components/SourceControlManagement/SyncTabs.tsx @@ -21,19 +21,30 @@ import { LocalPipelineListView } from './LocalPipelineListView'; import styled from 'styled-components'; import T from 'i18n-react'; import { RemotePipelineListView } from './RemotePipelineListView'; -import { FeatureProvider } from 'services/react/providers/featureFlagProvider'; -import { getNamespacePipelineList, getRemotePipelineList } from './store/ActionCreator'; +import { + getNamespacePipelineList, + getRemotePipelineList, + setSyncStatusOfAllPipelines, +} from './store/ActionCreator'; import { getCurrentNamespace } from 'services/NamespaceStore'; +import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; +import { OperationsHistoryView } from './OperationsHistoryView'; const PREFIX = 'features.SourceControlManagement'; const StyledDiv = styled.div` - padding: 10px; - margin-top: 10px; + padding: 4px 10px; +`; + +const StyledTabs = styled(Tabs)` + border-bottom: 1px solid #e8e8e8; `; const ScmSyncTabs = () => { const [tabIndex, setTabIndex] = useState(0); + const multiPushEnabled = useFeatureFlagDefaultFalse( + 'source.control.management.multi.app.enabled' + ); const { ready: pushStateReady, nameFilter } = useSelector(({ push }) => push); useEffect(() => { @@ -49,6 +60,12 @@ const ScmSyncTabs = () => { } }, [pullStateReady]); + useEffect(() => { + if (pushStateReady && pullStateReady) { + setSyncStatusOfAllPipelines(); + } + }, [pushStateReady, pullStateReady]); + const handleTabChange = (e, newValue) => { setTabIndex(newValue); // refetch latest pipeline data, while displaying possibly stale data @@ -59,10 +76,26 @@ const ScmSyncTabs = () => { } }; + const renderTabContent = () => { + if (tabIndex === 0) { + return ; + } + + if (tabIndex === 1) { + return ; + } + + if (tabIndex === 2) { + return ; + } + + return null; + }; + return ( <> - { > - + {multiPushEnabled && ( + + )} + - - - {tabIndex === 0 ? : } - - + {renderTabContent()} ); }; diff --git a/app/cdap/components/SourceControlManagement/helpers.ts b/app/cdap/components/SourceControlManagement/helpers.ts index a6ac2dbe6b9..9dccfa22513 100644 --- a/app/cdap/components/SourceControlManagement/helpers.ts +++ b/app/cdap/components/SourceControlManagement/helpers.ts @@ -21,7 +21,11 @@ import { IResource, IOperationResource, IOperationRun, + IOperationError, IOperationResourceScopedErrorMessage, + IRepositoryPipeline, + TSyncStatusFilter, + TSyncStatus, } from './types'; import T from 'i18n-react'; import { ITimeInstant, timeInstantToString } from 'services/DataFormatter'; @@ -71,7 +75,7 @@ export const getOperationRunMessage = (operation: IOperationRun) => { return T.translate(`${PREFIX}.syncSuccessMulti`); } - if (operation.status === OperationStatus.FAILED || operation.status === OperationStatus.KILLED) { + if (operation.status === OperationStatus.FAILED) { if (operation.type === OperationType.PUSH_APPS) { return T.translate(`${PREFIX}.push.pushFailureMulti`, { n }); } @@ -81,6 +85,16 @@ export const getOperationRunMessage = (operation: IOperationRun) => { return T.translate(`${PREFIX}.syncFailreMulti`); } + if (operation.status === OperationStatus.KILLED) { + if (operation.type === OperationType.PUSH_APPS) { + return T.translate(`${PREFIX}.push.pushKilledMulti`, { n }); + } + if (operation.type === OperationType.PULL_APPS) { + return T.translate(`${PREFIX}.pull.pullKilledMulti`, { n }); + } + return T.translate(`${PREFIX}.syncKilledMulti`); + } + if (operation.type === OperationType.PUSH_APPS) { return T.translate(`${PREFIX}.push.pushAppMessageMulti`, { n }); } @@ -99,10 +113,14 @@ export const getOperationStatusType = (operation: IOperationRun) => { return 'success'; } - if (operation.status === OperationStatus.FAILED || operation.status === OperationStatus.KILLED) { + if (operation.status === OperationStatus.FAILED) { return 'error'; } + if (operation.status === OperationStatus.KILLED) { + return 'warning'; + } + return 'info'; }; @@ -154,3 +172,51 @@ export const getOperationRunTime = (operation: IOperationRun): string => { } return null; }; + +const getSyncStatusWeight = (syncStatus?: TSyncStatus): number => { + if (syncStatus === undefined) { + return 0; + } + if (syncStatus === 'not_available') { + return 0; + } + if (syncStatus === 'not_connected') { + return 0; + } + if (syncStatus === 'out_of_sync') { + return 1; + } + return 2; +}; + +export const compareSyncStatus = (a: IRepositoryPipeline, b: IRepositoryPipeline): number => { + return getSyncStatusWeight(a.syncStatus) - getSyncStatusWeight(b.syncStatus); +}; + +export const stableSort = (array, comparator) => { + const stabilizedArray = array.map((el, index) => [el, index]); + stabilizedArray.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + }); + return stabilizedArray.map((el) => el[0]); +}; + +export const filterOnSyncStatus = ( + array: IRepositoryPipeline[], + syncStatusFilter: TSyncStatusFilter +): IRepositoryPipeline[] => { + if (syncStatusFilter === 'all') { + return array; + } + + return array.filter((pipeline) => { + if (syncStatusFilter === 'out_of_sync') { + return ['not_connected', 'out_of_sync', 'not_available'].includes(pipeline.syncStatus); + } + return pipeline.syncStatus === syncStatusFilter; + }); +}; diff --git a/app/cdap/components/SourceControlManagement/index.tsx b/app/cdap/components/SourceControlManagement/index.tsx index 6f4f5229408..ca9146fdf8f 100644 --- a/app/cdap/components/SourceControlManagement/index.tsx +++ b/app/cdap/components/SourceControlManagement/index.tsx @@ -15,7 +15,7 @@ */ import { EntityTopPanel } from 'components/EntityTopPanel'; -import React from 'react'; +import React, { useEffect } from 'react'; import { Provider } from 'react-redux'; import SourceControlManagementSyncStore from './store'; import T from 'i18n-react'; @@ -24,11 +24,15 @@ import { useHistory } from 'react-router'; import { useOnUnmount } from 'services/react/customHooks/useOnUnmount'; import { reset, resetRemote } from './store/ActionCreator'; import ScmSyncTabs from './SyncTabs'; +import { useHideFooterInPage } from 'components/FooterContext'; +import { FeatureProvider } from 'services/react/providers/featureFlagProvider'; const PREFIX = 'features.SourceControlManagement'; const SourceControlManagementSyncView = () => { const history = useHistory(); + useHideFooterInPage(); + useOnUnmount(() => { resetRemote(); reset(); @@ -48,7 +52,9 @@ const SourceControlManagementSyncView = () => { history.push(closeAndBackLink); }} /> - + + + ); }; diff --git a/app/cdap/components/SourceControlManagement/store/ActionCreator.ts b/app/cdap/components/SourceControlManagement/store/ActionCreator.ts index e4250eac356..ba30d4da3b9 100644 --- a/app/cdap/components/SourceControlManagement/store/ActionCreator.ts +++ b/app/cdap/components/SourceControlManagement/store/ActionCreator.ts @@ -27,7 +27,14 @@ import SourceControlManagementSyncStore, { } from '.'; import { SourceControlApi } from 'api/sourcecontrol'; import { LongRunningOperationApi } from 'api/longRunningOperation'; -import { IPipeline, IPushResponse, IRepositoryPipeline, IOperationRun } from '../types'; +import { + IPipeline, + IPushResponse, + IRepositoryPipeline, + IOperationRun, + TSyncStatusFilter, + TSyncStatus, +} from '../types'; import { SUPPORT } from 'components/StatusButton/constants'; import { compareTimeInstant } from '../helpers'; import { getCurrentNamespace } from 'services/NamespaceStore'; @@ -36,6 +43,7 @@ const PREFIX = 'features.SourceControlManagement'; // push actions export const getNamespacePipelineList = (namespace, nameFilter = null) => { + const shouldCalculateSyncStatus = SourceControlManagementSyncStore.getState().pull.ready; MyPipelineApi.list({ namespace, artifactName: BATCH_PIPELINE_TYPE, @@ -44,13 +52,18 @@ export const getNamespacePipelineList = (namespace, nameFilter = null) => { (res: IPipeline[] | { applications: IPipeline[] }) => { const pipelines = Array.isArray(res) ? res : res?.applications; const nsPipelines = pipelines.map((pipeline) => { - return { + const localPipeline: IRepositoryPipeline = { name: pipeline.name, fileHash: pipeline.sourceControlMeta?.fileHash, lastSyncDate: pipeline.sourceControlMeta?.lastSyncedAt, error: null, status: null, + syncStatus: 'not_available', }; + if (shouldCalculateSyncStatus) { + localPipeline.syncStatus = getSyncStatus(localPipeline); + } + return localPipeline; }); setLocalPipelines(nsPipelines); }, @@ -60,6 +73,55 @@ export const getNamespacePipelineList = (namespace, nameFilter = null) => { ); }; +const getSyncStatus = (pipeline: IRepositoryPipeline, withLocal?: boolean): TSyncStatus => { + const listOfPipelines = withLocal + ? SourceControlManagementSyncStore.getState().push.localPipelines + : SourceControlManagementSyncStore.getState().pull.remotePipelines; + + const pipelineInList = listOfPipelines.find((p) => p.name === pipeline.name); + if (!pipeline.fileHash || !pipelineInList) { + return 'not_connected'; + } + return pipeline.fileHash === pipelineInList.fileHash ? 'in_sync' : 'out_of_sync'; +}; + +export const setSyncStatusOfLocalPipelines = () => { + const localPipelines = SourceControlManagementSyncStore.getState().push.localPipelines; + const remotePipelines = SourceControlManagementSyncStore.getState().pull.remotePipelines; + if (!remotePipelines.length) { + return; + } + + setLocalPipelines( + localPipelines.map((pipeline) => { + const p = { ...pipeline }; + p.syncStatus = getSyncStatus(pipeline); + return p; + }) + ); +}; + +export const setSyncStatusOfRemotePipelines = () => { + const localPipelines = SourceControlManagementSyncStore.getState().push.localPipelines; + const remotePipelines = SourceControlManagementSyncStore.getState().pull.remotePipelines; + if (!localPipelines.length) { + return; + } + + setRemotePipelines( + remotePipelines.map((pipeline) => { + const p = { ...pipeline }; + p.syncStatus = getSyncStatus(pipeline, true); + return p; + }) + ); +}; + +export const setSyncStatusOfAllPipelines = () => { + setSyncStatusOfLocalPipelines(); + setSyncStatusOfRemotePipelines(); +}; + export const setLocalPipelines = (pipelines: IRepositoryPipeline[]) => { SourceControlManagementSyncStore.dispatch({ type: PushToGitActions.setLocalPipelines, @@ -86,6 +148,15 @@ export const setNameFilter = (nameFilter: string) => { debouncedApplySearch(); }; +export const setSyncStatusFilter = (syncStatusFilter: TSyncStatusFilter) => { + SourceControlManagementSyncStore.dispatch({ + type: PushToGitActions.setSyncStatusFilter, + payload: { + syncStatusFilter, + }, + }); +}; + export const setSelectedPipelines = (selectedPipelines: any[]) => { SourceControlManagementSyncStore.dispatch({ type: PushToGitActions.setSelectedPipelines, @@ -199,18 +270,24 @@ export const reset = () => { // pull actions export const getRemotePipelineList = (namespace) => { + const shouldCalculateSyncStatus = SourceControlManagementSyncStore.getState().push.ready; SourceControlApi.list({ namespace, }).subscribe( (res: IRepositoryPipeline[] | { apps: IRepositoryPipeline[] }) => { const pipelines = Array.isArray(res) ? res : res?.apps; const remotePipelines = pipelines.map((pipeline) => { - return { + const remotePipeline: IRepositoryPipeline = { name: pipeline.name, fileHash: pipeline.fileHash, error: null, status: null, + syncStatus: 'not_available', }; + if (shouldCalculateSyncStatus) { + remotePipeline.syncStatus = getSyncStatus(remotePipeline, true); + } + return remotePipeline; }); setRemotePipelines(remotePipelines); }, @@ -248,6 +325,15 @@ export const setRemoteNameFilter = (nameFilter: string) => { }); }; +export const setRemoteSyncStatusFilter = (syncStatusFilter: TSyncStatusFilter) => { + SourceControlManagementSyncStore.dispatch({ + type: PullFromGitActions.setSyncStatusFilter, + payload: { + syncStatusFilter, + }, + }); +}; + export const setSelectedRemotePipelines = (selectedPipelines: string[]) => { SourceControlManagementSyncStore.dispatch({ type: PullFromGitActions.setSelectedPipelines, @@ -348,11 +434,30 @@ export const resetRemote = () => { }); }; +const updateOperationsHistory = (operations: IOperationRun[]) => { + const currentHistory = [ + ...SourceControlManagementSyncStore.getState().operationRun.allOperations, + ]; + const operationIds = new Set(currentHistory.map((op) => op.id)); + for (const operation of operations) { + if (!operationIds.has(operation.id)) { + currentHistory.push(operation); + } + } + + currentHistory.sort((a, b) => compareTimeInstant(b.metadata.createTime, a.metadata.createTime)); + SourceControlManagementSyncStore.dispatch({ + type: OperationRunActions.setAllOperations, + payload: currentHistory, + }); +}; + export const setLatestOperation = (namespace: string, operation: IOperationRun) => { SourceControlManagementSyncStore.dispatch({ type: OperationRunActions.setLatestOperation, payload: operation, }); + updateOperationsHistory([operation]); if (operation.done) { return; @@ -364,7 +469,7 @@ export const setLatestOperation = (namespace: string, operation: IOperationRun) }).subscribe((res: IOperationRun) => { if (res.done) { pollOperationStatus.unsubscribe(); - + updateOperationsHistory([res]); SourceControlManagementSyncStore.dispatch({ type: OperationRunActions.setLatestOperation, payload: res, @@ -388,6 +493,8 @@ export const fetchLatestPullOperation = (namespace: string) => { if (res.operations.length < 1) { return; } + updateOperationsHistory(res.operations); + const operation = res.operations[0]; if ( !currentLatest || @@ -408,6 +515,8 @@ export const fetchLatestPushOperation = (namespace: string) => { if (res.operations.length < 1) { return; } + updateOperationsHistory(res.operations); + const operation = res.operations[0]; if ( !currentLatest || @@ -439,3 +548,10 @@ export const refetchAllPipelines = () => { getNamespacePipelineList(getCurrentNamespace()); getRemotePipelineList(getCurrentNamespace()); }; + +export const dismissOperationAlert = () => { + SourceControlManagementSyncStore.dispatch({ + type: OperationRunActions.setShowLastOperationInfo, + payload: false, + }); +}; diff --git a/app/cdap/components/SourceControlManagement/store/index.ts b/app/cdap/components/SourceControlManagement/store/index.ts index 9591d223ac5..047d132651c 100644 --- a/app/cdap/components/SourceControlManagement/store/index.ts +++ b/app/cdap/components/SourceControlManagement/store/index.ts @@ -17,13 +17,13 @@ import { combineReducers, createStore, Store as StoreInterface } from 'redux'; import { composeEnhancers } from 'services/helpers'; import { IAction } from 'services/redux-helpers'; -import { IOperationRun, IRepositoryPipeline } from '../types'; -import { act } from 'react-dom/test-utils'; +import { IOperationRun, IRepositoryPipeline, TSyncStatusFilter } from '../types'; interface IPushViewState { ready: boolean; localPipelines: IRepositoryPipeline[]; nameFilter: string; + syncStatusFilter: TSyncStatusFilter; selectedPipelines: string[]; commitModalOpen: boolean; loadingMessage: string; @@ -34,6 +34,7 @@ interface IPullViewState { ready: boolean; remotePipelines: IRepositoryPipeline[]; nameFilter: string; + syncStatusFilter: TSyncStatusFilter; selectedPipelines: string[]; loadingMessage: string; showFailedOnly: boolean; @@ -43,6 +44,8 @@ interface IPullViewState { interface IOperationRunState { running: boolean; operation?: IOperationRun; + showLastOperationInfo: boolean; + allOperations: IOperationRun[]; } interface IStore { @@ -55,6 +58,7 @@ export const PushToGitActions = { setLocalPipelines: 'LOCAL_PIPELINES_SET', reset: 'LOCAL_PIPELINES_RESET', setNameFilter: 'LOCAL_PIPELINES_SET_NAME_FILTER', + setSyncStatusFilter: 'LOCAL_PIPELINES_SET_SYNC_STATUS_FILTER', applySearch: 'LOCAL_PIPELINES_APPLY_SERACH', setSelectedPipelines: 'LOCAL_PIPELINES_SET_SELECTED_PIPELINES', toggleCommitModal: 'LOCAL_PIPELINES_TOGGLE_COMMIT_MODAL', @@ -66,6 +70,7 @@ export const PullFromGitActions = { setRemotePipelines: 'REMOTE_PIPELINES_SET', reset: 'REMOTE_PIPELINES_RESET', setNameFilter: 'REMOTE_PIPELINES_SET_NAME_FILTER', + setSyncStatusFilter: 'REMOTE_PIPELINES_SET_SYNC_STATUS_FILTER', applySearch: 'REMOTE_PIPELINES_APPLY_SERACH', setSelectedPipelines: 'REMOTE_PIPELINES_SET_SELECTED_PIPELINES', setLoadingMessage: 'REMOTE_PIPELINES_SET_LOADING_MESSAGE', @@ -76,12 +81,15 @@ export const PullFromGitActions = { export const OperationRunActions = { setLatestOperation: 'SET_RUNNING_OPERATION', unsetLatestOperation: 'UNSET_RUNNING_OPERATION', + setShowLastOperationInfo: 'SET_SHOW_LAST_OPERATION_INFO', + setAllOperations: 'SET_ALL_OPERATIONS', }; const defaultPushViewState: IPushViewState = { ready: false, localPipelines: [], nameFilter: '', + syncStatusFilter: 'all', selectedPipelines: [], commitModalOpen: false, loadingMessage: null, @@ -92,6 +100,7 @@ const defaultPullViewState: IPullViewState = { ready: false, remotePipelines: [], nameFilter: '', + syncStatusFilter: 'all', selectedPipelines: [], loadingMessage: null, showFailedOnly: false, @@ -100,6 +109,8 @@ const defaultPullViewState: IPullViewState = { const defaultOperationRunState: IOperationRunState = { running: false, + showLastOperationInfo: true, + allOperations: [], }; const push = (state = defaultPushViewState, action: IAction) => { @@ -115,6 +126,11 @@ const push = (state = defaultPushViewState, action: IAction) => { ...state, nameFilter: action.payload.nameFilter, }; + case PushToGitActions.setSyncStatusFilter: + return { + ...state, + syncStatusFilter: action.payload.syncStatusFilter, + }; case PushToGitActions.applySearch: return { ...defaultPushViewState, @@ -161,6 +177,11 @@ const pull = (state = defaultPullViewState, action: IAction) => { ...state, nameFilter: action.payload.nameFilter, }; + case PullFromGitActions.setSyncStatusFilter: + return { + ...state, + syncStatusFilter: action.payload.syncStatusFilter, + }; case PullFromGitActions.setPullViewErrorMsg: return { ...state, @@ -201,12 +222,26 @@ const operationRun = ( switch (action.type) { case OperationRunActions.setLatestOperation: return { + ...state, running: !action.payload?.done, operation: action.payload, + showLastOperationInfo: true, }; case OperationRunActions.unsetLatestOperation: return { + ...state, running: false, + showLastOperationInfo: false, + }; + case OperationRunActions.setShowLastOperationInfo: + return { + ...state, + showLastOperationInfo: action.payload, + }; + case OperationRunActions.setAllOperations: + return { + ...state, + allOperations: action.payload, }; default: return state; diff --git a/app/cdap/components/SourceControlManagement/styles.ts b/app/cdap/components/SourceControlManagement/styles.ts index a20e5289c79..0baf7cc0039 100644 --- a/app/cdap/components/SourceControlManagement/styles.ts +++ b/app/cdap/components/SourceControlManagement/styles.ts @@ -22,8 +22,8 @@ export const TableBox = styled(TableContainer)` box-shadow: 0 1px 2px 0 rgb(0 0 0 / 20%); margin-top: 10px; margin-bottom: 30px; - max-height: calc(80vh - 200px); - min-height: calc(80vh - 300px); + max-height: calc(80vh - ${(props) => (props.lastOperationInfoShown ? '280px' : '200px')}); + min-height: calc(80vh - ${(props) => (props.lastOperationInfoShown ? '380px' : '200px')}); `; export const StyledTableCell = styled(TableCell)` @@ -82,3 +82,16 @@ export const FailStatusDiv = styled.div` export const AlertErrorView = styled.p` margin: 1rem 0; `; + +export const FiltersAndStatusWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +`; + +export const SyncStatusWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; diff --git a/app/cdap/components/SourceControlManagement/types.ts b/app/cdap/components/SourceControlManagement/types.ts index c1b17e4324b..7dff7e68e68 100644 --- a/app/cdap/components/SourceControlManagement/types.ts +++ b/app/cdap/components/SourceControlManagement/types.ts @@ -20,12 +20,16 @@ import { OperationType } from './OperationType'; import { OperationStatus } from './OperationStatus'; import { ITimeInstant } from 'services/DataFormatter'; +export type TSyncStatusFilter = 'all' | 'in_sync' | 'out_of_sync'; +export type TSyncStatus = 'not_available' | 'in_sync' | 'out_of_sync' | 'not_connected'; + export interface IRepositoryPipeline { name: string; fileHash: string; error: string; status: SUPPORT; lastSyncDate?: ITimeInstant; + syncStatus?: TSyncStatus; } export interface IOperationResource { diff --git a/app/cdap/main.js b/app/cdap/main.js index 384fce67502..807d15e119b 100644 --- a/app/cdap/main.js +++ b/app/cdap/main.js @@ -19,7 +19,7 @@ import 'react-hot-loader/patch'; import './globals'; import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; -import React, { Component } from 'react'; +import React, { Component, useEffect, useState } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import SessionTokenStore, { fetchSessionToken } from 'services/SessionTokenStore'; import { Theme, applyTheme } from 'services/ThemeHelper'; @@ -65,6 +65,7 @@ import history from 'services/history'; import { CookieBanner } from 'components/CookieBanner'; // See ./graphql/fragements/README.md import introspectionQueryResultData from '../../graphql/fragments/fragmentTypes.json'; +import { FooterContext } from 'components/FooterContext'; require('../ui-utils/url-generator'); require('font-awesome-sass-loader!./styles/font-awesome.config.js'); @@ -409,7 +410,7 @@ class CDAP extends Component { ))} )} -