From afe28ce09bd7ed3c0b1905b89e7eb29531f7dd15 Mon Sep 17 00:00:00 2001 From: GnsP Date: Mon, 20 Nov 2023 21:44:15 +0530 Subject: [PATCH] ux improvements --- .../SourceControlManagement/index.tsx | 13 +- .../PullPipelineWizard.tsx | 2 +- .../LocalPipelineListView/PipelineTable.tsx | 182 ++++++++++++------ .../LocalPipelineListView/index.tsx | 149 +++++--------- .../OperationAlert.tsx | 147 ++++++++++++++ .../RemotePipelineTable.tsx | 161 ++++++++++------ .../RemotePipelineListView/index.tsx | 152 ++++++--------- .../SyncStatusFilters.tsx | 76 ++++++++ .../SourceControlManagement/SyncTabs.tsx | 94 +++++++++ .../SourceControlManagement/helpers.ts | 85 +++++++- .../SourceControlManagement/index.tsx | 47 ++--- .../store/ActionCreator.ts | 92 ++++++++- .../SourceControlManagement/store/index.ts | 19 +- .../SourceControlManagement/styles.ts | 7 + .../SourceControlManagement/types.ts | 9 + app/cdap/text/text-en.yaml | 10 + 16 files changed, 887 insertions(+), 358 deletions(-) create mode 100644 app/cdap/components/SourceControlManagement/OperationAlert.tsx create mode 100644 app/cdap/components/SourceControlManagement/SyncStatusFilters.tsx create mode 100644 app/cdap/components/SourceControlManagement/SyncTabs.tsx diff --git a/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx b/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx index 7cb436b9816..cc394d218d8 100644 --- a/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx +++ b/app/cdap/components/NamespaceAdmin/SourceControlManagement/index.tsx @@ -26,7 +26,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'; @@ -34,6 +33,7 @@ import { getCurrentNamespace } from 'services/NamespaceStore'; import { getSourceControlManagement } from '../store/ActionCreator'; import Alert from 'components/shared/Alert'; import ButtonLoadingHoc from 'components/shared/Buttons/ButtonLoadingHoc'; +import { useHistory } from 'react-router'; const PrimaryTextLoadingButton = ButtonLoadingHoc(PrimaryTextButton); @@ -55,6 +55,8 @@ export const SourceControlManagement = () => { const sourceControlManagementConfig: ISourceControlManagementConfig = useSelector( (state) => state.sourceControlManagementConfig ); + const history = useHistory(); + const toggleForm = () => { setIsFormOpen(!isFormOpen); }; @@ -85,11 +87,16 @@ export const SourceControlManagement = () => { ]; const validateConfigAndRedirect = () => { - setLoading(true); const namespace = getCurrentNamespace(); + if (sourceControlManagementConfig) { + history.push(`/ns/${namespace}/scm/sync`); + return; + } + + setLoading(true); getSourceControlManagement(namespace).subscribe( () => { - window.location.href = `/ns/${namespace}/scm/sync`; + history.push(`/ns/${namespace}/scm/sync`); }, (err) => { setErrorMessage(err.message); 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 0284d185ca3..26e68ef8d77 100644 --- a/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx +++ b/app/cdap/components/SourceControlManagement/LocalPipelineListView/PipelineTable.tsx @@ -14,11 +14,19 @@ * 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, +} from '@material-ui/core'; import InfoIcon from '@material-ui/icons/Info'; 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'; @@ -30,6 +38,8 @@ import { StyledFixedWidthCell, StyledPopover, } from '../styles'; +import { compareSyncStatus, filterOnSyncStatus, stableSort } from '../helpers'; +import LoadingSVG from 'components/shared/LoadingSVG'; const PREFIX = 'features.SourceControlManagement.table'; @@ -37,18 +47,28 @@ interface IRepositoryPipelineTableProps { localPipelines: IRepositoryPipeline[]; selectedPipelines: string[]; showFailedOnly: boolean; - enableMultipleSelection?: boolean; + multiPushEnabled?: boolean; disabled?: boolean; + syncStatusFilter?: TSyncStatusFilter; } export const LocalPipelineTable = ({ localPipelines, selectedPipelines, showFailedOnly, - enableMultipleSelection = false, + multiPushEnabled = false, disabled = false, + syncStatusFilter = 'all', }: IRepositoryPipelineTableProps) => { const isSelected = (name: string) => selectedPipelines.indexOf(name) !== -1; + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const filteredPipelines = filterOnSyncStatus(localPipelines, syncStatusFilter); + const filteredPipelineNames = filteredPipelines.map((pipeline) => pipeline.name); + const selectedPipelinesSet = new Set(selectedPipelines); + const isAllFilteredPipelinesSelected = filteredPipelineNames.reduce((acc, pipelineName) => { + return acc && selectedPipelinesSet.has(pipelineName); + }, true); const handleSelectAllClick = (event: React.ChangeEvent) => { if (disabled) { @@ -56,8 +76,7 @@ export const LocalPipelineTable = ({ } if (event.target.checked) { - const allSelected = localPipelines.map((pipeline) => pipeline.name); - setSelectedPipelines(allSelected); + setSelectedPipelines(filteredPipelineNames); return; } setSelectedPipelines([]); @@ -68,7 +87,7 @@ export const LocalPipelineTable = ({ return; } - if (enableMultipleSelection) { + if (multiPushEnabled) { handleMultipleSelection(name); return; } @@ -97,19 +116,26 @@ export const LocalPipelineTable = ({ setSelectedPipelines(newSelected); }; + const handleSort = () => { + const isAsc = sortOrder === 'asc'; + setSortOrder(isAsc ? 'desc' : 'asc'); + }; + + const syncStatusComparator = (a: IRepositoryPipeline, b: IRepositoryPipeline) => { + return sortOrder === 'desc' ? compareSyncStatus(a, b) : -compareSyncStatus(a, b); + }; + return ( - {enableMultipleSelection && ( + {multiPushEnabled && ( 0 && selectedPipelines.length < localPipelines.length - } - checked={selectedPipelines.length === localPipelines.length} + indeterminate={selectedPipelines.length > 0 && !isAllFilteredPipelinesSelected} + checked={isAllFilteredPipelinesSelected} onChange={handleSelectAllClick} disabled={disabled} /> @@ -117,62 +143,90 @@ export const LocalPipelineTable = ({ {T.translate(`${PREFIX}.pipelineName`)} - -
- {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) => { - if (showFailedOnly && !pipeline.error) { - // only render pipelines that failed to push - return; - } - const isPipelineSelected = isSelected(pipeline.name); - return ( - handleClick(e, pipeline.name)} - data-testid={`local-${pipeline.name}`} - disabled={disabled} - > - - - - { + if (showFailedOnly && !pipeline.error) { + // only render pipelines that failed to push + return; + } + const isPipelineSelected = isSelected(pipeline.name); + return ( + handleClick(e, pipeline.name)} + data-testid={`local-${pipeline.name}`} + disabled={disabled} > - {pipeline.status !== null && ( - + + + + + {pipeline.status !== null && ( + + )} + + {pipeline.name} + {multiPushEnabled && ( + + {pipeline.syncStatus === undefined || + pipeline.syncStatus === 'not_available' ? ( + + ) : pipeline.syncStatus === 'not_connected' || + pipeline.syncStatus === 'out_of_sync' ? ( + T.translate(`${PREFIX}.gitSyncStatusUnsynced`) + ) : ( + T.translate(`${PREFIX}.gitSyncStatusSynced`) + )} + )} - - {pipeline.name} - - {pipeline.fileHash ? T.translate(`${PREFIX}.connected`) : '--'} - - - ); - })} + {!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 5b57b300607..598e9d022da 100644 --- a/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx +++ b/app/cdap/components/SourceControlManagement/LocalPipelineListView/index.tsx @@ -15,23 +15,22 @@ */ 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 Alert from '@material-ui/lab/Alert'; -import AlertTitle from '@material-ui/lab/AlertTitle'; import { countPushFailedPipelines, fetchLatestOperation, getNamespacePipelineList, pushMultipleSelectedPipelines, pushSelectedPipelines, - reset, resetPushStatus, setLoadingMessage, setLocalPipelines, setNameFilter, - stopOperation, + setSyncStatusFilter, + setSyncStatusOfAllPipelines, + setSyncStatusOfRemotePipelines, toggleCommitModal, toggleShowFailedOnly, } from '../store/ActionCreator'; @@ -42,30 +41,21 @@ import { CommitModal } from './CommitModal'; import cloneDeep from 'lodash/cloneDeep'; import PrimaryTextButton from 'components/shared/Buttons/PrimaryTextButton'; import { LocalPipelineTable } from './PipelineTable'; -import { useOnUnmount } from 'services/react/customHooks/useOnUnmount'; import { - AlertErrorView, FailStatusDiv, + FiltersAndStatusWrapper, PipelineListContainer, StyledSelectionStatusDiv, } from '../styles'; -import { IListResponse, IOperationMetaResponse, IOperationRun } from '../types'; +import { IListResponse, IOperationMetaResponse } from '../types'; import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; -import { - getOperationRunMessage, - getOperationStartTime, - getOperationStatusType, - parseOperationResource, -} from '../helpers'; -import Button from '@material-ui/core/Button'; -import { OperationStatus } from '../OperationStatus'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; +import { parseOperationResource } from '../helpers'; +import { OperationAlert } from '../OperationAlert'; +import { SyncStatusFilters } from '../SyncStatusFilters'; const PREFIX = 'features.SourceControlManagement.push'; export const LocalPipelineListView = () => { - const [viewErrorExpanded, setViewErrorExpanded] = useState(false); const { ready, localPipelines, @@ -74,6 +64,7 @@ export const LocalPipelineListView = () => { commitModalOpen, loadingMessage, showFailedOnly, + syncStatusFilter, } = useSelector(({ push }) => push); const { running: isAnOperationRunning, operation } = useSelector( @@ -97,8 +88,6 @@ export const LocalPipelineListView = () => { } }, []); - useOnUnmount(() => reset()); - const onPushSubmit = (commitMessage: string) => { resetPushStatus(); const pushedPipelines = cloneDeep(localPipelines); @@ -125,6 +114,7 @@ export const LocalPipelineListView = () => { }, complete() { setLoadingMessage(null); + setSyncStatusOfAllPipelines(); }, }); @@ -148,6 +138,7 @@ export const LocalPipelineListView = () => { }, complete() { setLoadingMessage(null); + setSyncStatusOfAllPipelines(); }, }); }; @@ -160,8 +151,9 @@ export const LocalPipelineListView = () => { localPipelines={localPipelines} selectedPipelines={selectedPipelines} showFailedOnly={showFailedOnly} - enableMultipleSelection={multiPushEnabled} + multiPushEnabled={multiPushEnabled} disabled={isAnOperationRunning} + syncStatusFilter={syncStatusFilter} /> { return
{T.translate(`${PREFIX}.emptyPipelineListMessage`, { query: nameFilter })}
; }; - const getOperationAction = () => { - if (!operation.done) { - return ( - - ); - } - - if (operation.status === OperationStatus.FAILED) { - return ( - - ); - } - - return undefined; - }; - return ( + {operation && multiPushEnabled && } - {operation && ( - - {getOperationRunMessage(operation)} - {getOperationStartTime(operation)} - {operation.status === OperationStatus.FAILED && viewErrorExpanded && ( - - Operation ID: {operation.id} -
- Error: {operation.error.message} -
- )} -
- )} - {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`)} - )} -
- )} + + {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() : } { + const [viewErrorExpanded, setViewErrorExpanded] = useState(false); + + const getOperationAction = () => { + if ( + operation.status === OperationStatus.STARTING || + operation.status === OperationStatus.RUNNING + ) { + return ( + + ); + } + + if (operation.status === OperationStatus.STOPPING) { + return ( + + ); + } + + if (operation.status === OperationStatus.FAILED) { + return ( + + + + ); + } + + return undefined; + }; + + const renderOperationTime = () => { + const startedPrefix = T.translate(`${PREFIX}.operationStartedAt`).toString(); + const startTime = getOperationStartTime(operation); + const timeTaken = getOperationRunTime(operation); + if (!timeTaken) { + return `${startedPrefix} ${startTime}`; + } + + const timeTakenPrefix = operation.done + ? T.translate(`${PREFIX}.operationRanFor`).toString() + : T.translate(`${PREFIX}.operationRunningFor`).toString(); + + return `${startedPrefix} ${startTime}, ${timeTakenPrefix} ${timeTaken}`; + }; + + function renderErrorMessage(message?: string) { + if (!message) { + return operation.error?.message; + } + + const firstColonIndex = message.indexOf(':'); + return message.substring(firstColonIndex + 1); + } + + return ( + + + {getOperationRunMessage(operation)} + {renderOperationTime()} + {operation.status === OperationStatus.FAILED && viewErrorExpanded && ( + + Operation ID: {operation.id} + {operation.error?.details[0] && ( + <> +
+ Pipeline Name: {parseOperationResource(operation.error?.details[0]).name} +
+ Error: {renderErrorMessage(operation.error?.details[0]?.message)} + + )} +
+ )} +
+
+ ); +}; diff --git a/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx b/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx index dca38c86905..3e871cede27 100644 --- a/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx +++ b/app/cdap/components/SourceControlManagement/RemotePipelineListView/RemotePipelineTable.tsx @@ -14,14 +14,24 @@ * 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, +} from '@material-ui/core'; 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, TableBox } from '../styles'; +import { compareSyncStatus, filterOnSyncStatus, stableSort } from '../helpers'; +import LoadingSVG from 'components/shared/LoadingSVG'; const PREFIX = 'features.SourceControlManagement.table'; @@ -29,25 +39,35 @@ interface IRepositoryPipelineTableProps { remotePipelines: IRepositoryPipeline[]; selectedPipelines: string[]; showFailedOnly: boolean; - enableMultipleSelection?: boolean; + multiPullEnabled?: boolean; disabled?: boolean; + syncStatusFilter?: TSyncStatusFilter; } export const RemotePipelineTable = ({ remotePipelines, selectedPipelines, showFailedOnly, - enableMultipleSelection = false, + multiPullEnabled = false, disabled = false, + syncStatusFilter = 'all', }: IRepositoryPipelineTableProps) => { const isSelected = (name: string) => selectedPipelines.indexOf(name) !== -1; + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + + const filteredPipelines = filterOnSyncStatus(remotePipelines, syncStatusFilter); + const filteredPipelineNames = filteredPipelines.map((pipeline) => pipeline.name); + const selectedPipelinesSet = new Set(selectedPipelines); + const isAllFilteredPipelinesSelected = filteredPipelineNames.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 +103,32 @@ export const RemotePipelineTable = ({ } if (event.target.checked) { - const allSelected = remotePipelines.map((pipeline) => pipeline.name); - setSelectedRemotePipelines(allSelected); + setSelectedRemotePipelines(filteredPipelineNames); return; } setSelectedRemotePipelines([]); }; + const handleSort = () => { + const isAsc = sortOrder === 'asc'; + setSortOrder(isAsc ? 'desc' : 'asc'); + }; + + const syncStatusComparator = (a: IRepositoryPipeline, b: IRepositoryPipeline) => { + return sortOrder === 'desc' ? compareSyncStatus(a, b) : -compareSyncStatus(a, b); + }; + return ( - {enableMultipleSelection && ( + {multiPullEnabled && ( 0 && - selectedPipelines.length < remotePipelines.length - } - checked={selectedPipelines.length === remotePipelines.length} + indeterminate={selectedPipelines.length > 0 && !isAllFilteredPipelinesSelected} + checked={isAllFilteredPipelinesSelected} onChange={handleSelectAllClick} disabled={disabled} /> @@ -111,52 +136,76 @@ export const RemotePipelineTable = ({ {T.translate(`${PREFIX}.pipelineName`)} + {multiPullEnabled && ( + + + {T.translate(`${PREFIX}.gitSyncStatus`)} + + + )} - {remotePipelines.map((pipeline: IRepositoryPipeline) => { - if (showFailedOnly && !pipeline.error) { - // only render pipelines that failed to pull - return; - } - const isPipelineSelected = isSelected(pipeline.name); - return ( - handleClick(e, pipeline.name)} - data-testid={`remote-${pipeline.name}`} - disabled={disabled} - > - - - - { + if (showFailedOnly && !pipeline.error) { + // only render pipelines that failed to pull + return; + } + const isPipelineSelected = isSelected(pipeline.name); + return ( + handleClick(e, pipeline.name)} + data-testid={`remote-${pipeline.name}`} + disabled={disabled} > - {pipeline.status !== null && ( - + + + + + {pipeline.status !== null && ( + + )} + + {pipeline.name} + {multiPullEnabled && ( + + {pipeline.syncStatus === undefined || + pipeline.syncStatus === 'not_available' ? ( + + ) : pipeline.syncStatus === 'not_connected' || + pipeline.syncStatus === 'out_of_sync' ? ( + T.translate(`${PREFIX}.gitSyncStatusUnsynced`) + ) : ( + T.translate(`${PREFIX}.gitSyncStatusSynced`) + )} + )} - - {pipeline.name} - - ); - })} + + ); + } + )}
diff --git a/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx b/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx index c2b9684e686..45d0a32524b 100644 --- a/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx +++ b/app/cdap/components/SourceControlManagement/RemotePipelineListView/index.tsx @@ -14,16 +14,14 @@ * 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 { default as MuiAlert } from '@material-ui/lab/Alert'; -import AlertTitle from '@material-ui/lab/AlertTitle'; import { SearchBox } from '../SearchBox'; import { - AlertErrorView, FailStatusDiv, + FiltersAndStatusWrapper, PipelineListContainer, StyledSelectionStatusDiv, } from '../styles'; @@ -34,45 +32,40 @@ import { pullAndDeploySelectedRemotePipelines, resetPullStatus, setPullViewErrorMsg, - resetRemote, setRemoteLoadingMessage, setRemoteNameFilter, setRemotePipelines, toggleRemoteShowFailedOnly, pullAndDeployMultipleSelectedRemotePipelines, fetchLatestOperation, - stopOperation, + setRemoteSyncStatusFilter, + setSyncStatusOfAllPipelines, } 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'; import Alert from 'components/shared/Alert'; import { useFeatureFlagDefaultFalse } from 'services/react/customHooks/useFeatureFlag'; -import { - getOperationRunMessage, - getOperationStartTime, - getOperationStatusType, - parseOperationResource, -} from '../helpers'; -import Button from '@material-ui/core/Button'; -import { OperationStatus } from '../OperationStatus'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; +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) => { - const [viewErrorExpanded, setViewErrorExpanded] = useState(false); +export const RemotePipelineListView = ({ + redirectOnSubmit, + singlePipelineMode, +}: IRemotePipelineListViewProps) => { const { ready, remotePipelines, @@ -81,15 +74,16 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList loadingMessage, showFailedOnly, pullViewErrorMsg, + syncStatusFilter, } = useSelector(({ pull }) => pull); const { running: isAnOperationRunning, operation } = 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(() => { @@ -104,8 +98,6 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList } }, []); - useOnUnmount(() => resetRemote()); - const filteredPipelines = remotePipelines.filter((pipeline) => pipeline.name.toLowerCase().includes(nameFilter.toLowerCase()) ); @@ -132,6 +124,7 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList }, complete() { setRemoteLoadingMessage(null); + setSyncStatusOfAllPipelines(); }, }); @@ -161,6 +154,7 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList }, complete() { setRemoteLoadingMessage(null); + setSyncStatusOfAllPipelines(); }, }); }; @@ -173,8 +167,9 @@ export const RemotePipelineListView = ({ redirectOnSubmit }: IRemotePipelineList remotePipelines={filteredPipelines} selectedPipelines={selectedPipelines} showFailedOnly={showFailedOnly} - enableMultipleSelection={multiPullEnabled} + multiPullEnabled={multiPullEnabled} disabled={isAnOperationRunning} + syncStatusFilter={syncStatusFilter} /> {T.translate(`${PREFIX}.emptyPipelineListMessage`, { query: nameFilter })}; }; - const getOperationAction = () => { - if (!operation.done) { - return ( - - ); - } - - if (operation.status === OperationStatus.FAILED) { - return ( - - ); - } - - return undefined; - }; - return ( <> setPullViewErrorMsg()} /> + {operation && multiPullEnabled && } - {operation && ( - - {getOperationRunMessage(operation)} - {getOperationStartTime(operation)} - {operation.status === OperationStatus.FAILED && viewErrorExpanded && ( - - Operation ID: {operation.id} -
- Error: {operation.error.message} -
- )} -
- )} - {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')} - - - )} -
- )} + + {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 new file mode 100644 index 00000000000..91a937baa67 --- /dev/null +++ b/app/cdap/components/SourceControlManagement/SyncTabs.tsx @@ -0,0 +1,94 @@ +/* + * Copyright © 2023 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 { Tab, Tabs } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +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, + setSyncStatusOfAllPipelines, +} from './store/ActionCreator'; +import { getCurrentNamespace } from 'services/NamespaceStore'; + +const PREFIX = 'features.SourceControlManagement'; + +const StyledDiv = styled.div` + padding: 10px; + margin-top: 10px; +`; + +const ScmSyncTabs = () => { + const [tabIndex, setTabIndex] = useState(0); + + const { ready: pushStateReady, nameFilter } = useSelector(({ push }) => push); + useEffect(() => { + if (!pushStateReady) { + getNamespacePipelineList(getCurrentNamespace(), nameFilter); + } + }, [pushStateReady]); + + const { ready: pullStateReady } = useSelector(({ pull }) => pull); + useEffect(() => { + if (!pullStateReady) { + getRemotePipelineList(getCurrentNamespace()); + } + }, [pullStateReady]); + + useEffect(() => { + if (pushStateReady && pullStateReady) { + setSyncStatusOfAllPipelines(); + } + }, [pushStateReady, pullStateReady]); + + const handleTabChange = (e, newValue) => { + setTabIndex(newValue); + // refetch latest pipeline data, while displaying possibly stale data + if (newValue === 0) { + getNamespacePipelineList(getCurrentNamespace(), nameFilter); + } else { + getRemotePipelineList(getCurrentNamespace()); + } + }; + + return ( + <> + + + + + + + + + {tabIndex === 0 ? : } + + + + ); +}; + +export default ScmSyncTabs; diff --git a/app/cdap/components/SourceControlManagement/helpers.ts b/app/cdap/components/SourceControlManagement/helpers.ts index c8657bb58c2..1e1e1003bbf 100644 --- a/app/cdap/components/SourceControlManagement/helpers.ts +++ b/app/cdap/components/SourceControlManagement/helpers.ts @@ -17,7 +17,17 @@ import moment from 'moment'; import { OperationStatus } from './OperationStatus'; import { OperationType } from './OperationType'; -import { IResource, IOperationResource, IOperationRun, ITimeInstant } from './types'; +import { + IResource, + IOperationResource, + IOperationRun, + ITimeInstant, + IOperationError, + IOperationResourceScopedErrorMessage, + IRepositoryPipeline, + TSyncStatusFilter, + TSyncStatus, +} from './types'; import T from 'i18n-react'; const PREFIX = 'features.SourceControlManagement'; @@ -34,6 +44,20 @@ export const parseOperationResource = (resource: IOperationResource): IResource }; }; +export const parseErrorMessage = (errorMessage: string): IOperationResourceScopedErrorMessage => { + const firstColonIndex = errorMessage.indexOf(':'); + if (firstColonIndex === -1) { + return { + message: errorMessage, + }; + } + + return { + type: errorMessage.substring(0, firstColonIndex).trim(), + message: errorMessage.substring(firstColonIndex + 1).trim(), + }; +}; + export const getOperationRunMessage = (operation: IOperationRun) => { const n = operation.metadata?.resources?.length || ''; @@ -123,3 +147,62 @@ export const compareTimeInstant = (t1: ITimeInstant, t2: ITimeInstant): number = export const getOperationStartTime = (operation: IOperationRun): string => { return moment(operation.metadata?.createTime.seconds * 1000).format('DD-MM-YYYY HH:mm:ss A'); }; + +export const getOperationRunTime = (operation: IOperationRun): string => { + if (operation.metadata?.createTime && operation.metadata?.endTime) { + return moment + .duration( + (operation.metadata?.endTime.seconds - operation.metadata?.createTime.seconds) * 1000 + ) + .humanize(); + } + 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 af6dad7d893..87b8143b80c 100644 --- a/app/cdap/components/SourceControlManagement/index.tsx +++ b/app/cdap/components/SourceControlManagement/index.tsx @@ -14,31 +14,25 @@ * the License. */ -import { Tab, Tabs } from '@material-ui/core'; import { EntityTopPanel } from 'components/EntityTopPanel'; -import React, { useState } from 'react'; -import { LocalPipelineListView } from './LocalPipelineListView'; +import React from 'react'; import { Provider } from 'react-redux'; import SourceControlManagementSyncStore from './store'; -import styled from 'styled-components'; import T from 'i18n-react'; -import { RemotePipelineListView } from './RemotePipelineListView'; import { getCurrentNamespace } from 'services/NamespaceStore'; -import { FeatureProvider } from 'services/react/providers/featureFlagProvider'; +import { resetRemote, reset } from './store/ActionCreator'; +import ScmSyncTabs from './SyncTabs'; +import { useOnUnmount } from 'services/react/customHooks/useOnUnmount'; +import { useHistory } from 'react-router'; const PREFIX = 'features.SourceControlManagement'; -const StyledDiv = styled.div` - padding: 10px; - margin-top: 10px; -`; - const SourceControlManagementSyncView = () => { - const [tabIndex, setTabIndex] = useState(0); - - const handleTabChange = (e, newValue) => { - setTabIndex(newValue); - }; + const history = useHistory(); + useOnUnmount(() => { + resetRemote(); + reset(); + }); const closeAndBackLink = `/ns/${getCurrentNamespace()}/details/scm`; @@ -47,29 +41,14 @@ const SourceControlManagementSyncView = () => { { - window.location.href = closeAndBackLink; + history.push(closeAndBackLink); }} breadCrumbAnchorLabel={T.translate('commons.namespaceAdmin').toString()} onBreadCrumbClick={() => { - window.location.href = closeAndBackLink; + history.push(closeAndBackLink); }} /> - - - - - - - - - {tabIndex === 0 ? : } - - + ); }; diff --git a/app/cdap/components/SourceControlManagement/store/ActionCreator.ts b/app/cdap/components/SourceControlManagement/store/ActionCreator.ts index 999ae6828ee..7c71222c316 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'; @@ -35,6 +42,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, @@ -43,12 +51,17 @@ 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, error: null, status: null, + syncStatus: 'not_available', }; + if (shouldCalculateSyncStatus) { + localPipeline.syncStatus = getSyncStatus(localPipeline); + } + return localPipeline; }); setLocalPipelines(nsPipelines); }, @@ -58,6 +71,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, @@ -84,6 +146,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, @@ -197,18 +268,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); }, @@ -246,6 +323,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, diff --git a/app/cdap/components/SourceControlManagement/store/index.ts b/app/cdap/components/SourceControlManagement/store/index.ts index 9591d223ac5..34ba8558e68 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; @@ -55,6 +56,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 +68,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', @@ -82,6 +85,7 @@ const defaultPushViewState: IPushViewState = { ready: false, localPipelines: [], nameFilter: '', + syncStatusFilter: 'all', selectedPipelines: [], commitModalOpen: false, loadingMessage: null, @@ -92,6 +96,7 @@ const defaultPullViewState: IPullViewState = { ready: false, remotePipelines: [], nameFilter: '', + syncStatusFilter: 'all', selectedPipelines: [], loadingMessage: null, showFailedOnly: false, @@ -115,6 +120,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 +171,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, diff --git a/app/cdap/components/SourceControlManagement/styles.ts b/app/cdap/components/SourceControlManagement/styles.ts index a20e5289c79..4fbbd72bcd8 100644 --- a/app/cdap/components/SourceControlManagement/styles.ts +++ b/app/cdap/components/SourceControlManagement/styles.ts @@ -82,3 +82,10 @@ 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; +`; diff --git a/app/cdap/components/SourceControlManagement/types.ts b/app/cdap/components/SourceControlManagement/types.ts index 952e80bdcbe..9201ffebecd 100644 --- a/app/cdap/components/SourceControlManagement/types.ts +++ b/app/cdap/components/SourceControlManagement/types.ts @@ -19,11 +19,15 @@ import { SUPPORT } from 'components/StatusButton/constants'; import { OperationType } from './OperationType'; import { OperationStatus } from './OperationStatus'; +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; + syncStatus?: TSyncStatus; } export interface IOperationResource { @@ -51,6 +55,11 @@ export interface IOperationResourceScopedError { message?: string; } +export interface IOperationResourceScopedErrorMessage { + type?: string; + message: string; +} + export interface IOperationRun { id: string; type: OperationType; diff --git a/app/cdap/text/text-en.yaml b/app/cdap/text/text-en.yaml index d2db1f1b5a9..e8a6b2fd963 100644 --- a/app/cdap/text/text-en.yaml +++ b/app/cdap/text/text-en.yaml @@ -3236,6 +3236,10 @@ features: pipelineSyncMessage: Syncing 1 pipeline with the remote repository pipelineSyncedSuccess: Successfully synced 1 pipeline with the remote repository pipelineSyncedFail: Failed to sync 1 pipeline with the remote repository + stopOperation: STOP + operationStartedAt: Started at + operationRanFor: Completed in + operationRunningFor: Running for pull: emptyPipelineListMessage: There are no pipelines in the remote repository or no pipelines matching the search query "{query}" modalTitle: Pull pipeline from remote repository @@ -3276,9 +3280,15 @@ features: tab: Local pipelines stopOperation: STOP table: + filtersLabel: Filters connected: Connected pipelineName: Pipeline name gitStatus: Connected to Git + gitSyncStatus: Sync Status + gitSyncStatusAll: All + gitSyncStatusSynced: In sync + gitSyncStatusUnsynced: Out of sync + gitSyncStatusNotConnected: Not connected gitStatusHelperText: This status indicates that the pipeline has been pushed to or pulled from the git repository in the past. It does not necessarily mean the content is up to date. pullFail: Failed to pull this pipeline from remote. pushFail: Failed to push this pipeline to remote.