diff --git a/app/cdap/components/SourceControlManagement/SearchBox.tsx b/app/cdap/components/SourceControlManagement/SearchBox.tsx
index a9842f7138b..cf252fa8099 100644
--- a/app/cdap/components/SourceControlManagement/SearchBox.tsx
+++ b/app/cdap/components/SourceControlManagement/SearchBox.tsx
@@ -56,12 +56,14 @@ export const SearchBox = ({ nameFilter, setNameFilter }: ISearchBoxProps) => {
),
- endAdornment: (
+ endAdornment: nameFilter ? (
setNameFilter('')}>
+ ) : (
+ undefined
),
}}
label={T.translate(`${PREFIX}.searchLabel`)}
diff --git a/app/cdap/components/SourceControlManagement/SyncTabs.tsx b/app/cdap/components/SourceControlManagement/SyncTabs.tsx
new file mode 100644
index 00000000000..833f4ba2fc5
--- /dev/null
+++ b/app/cdap/components/SourceControlManagement/SyncTabs.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 } 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]);
+
+ 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..a6ac2dbe6b9 100644
--- a/app/cdap/components/SourceControlManagement/helpers.ts
+++ b/app/cdap/components/SourceControlManagement/helpers.ts
@@ -17,8 +17,14 @@
import moment from 'moment';
import { OperationStatus } from './OperationStatus';
import { OperationType } from './OperationType';
-import { IResource, IOperationResource, IOperationRun, ITimeInstant } from './types';
+import {
+ IResource,
+ IOperationResource,
+ IOperationRun,
+ IOperationResourceScopedErrorMessage,
+} from './types';
import T from 'i18n-react';
+import { ITimeInstant, timeInstantToString } from 'services/DataFormatter';
const PREFIX = 'features.SourceControlManagement';
@@ -34,6 +40,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 || '';
@@ -121,5 +141,16 @@ 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');
+ return timeInstantToString(operation.metadata?.createTime);
+};
+
+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;
};
diff --git a/app/cdap/components/SourceControlManagement/index.tsx b/app/cdap/components/SourceControlManagement/index.tsx
index af6dad7d893..6f4f5229408 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 { useHistory } from 'react-router';
+import { useOnUnmount } from 'services/react/customHooks/useOnUnmount';
+import { reset, resetRemote } from './store/ActionCreator';
+import ScmSyncTabs from './SyncTabs';
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..e4250eac356 100644
--- a/app/cdap/components/SourceControlManagement/store/ActionCreator.ts
+++ b/app/cdap/components/SourceControlManagement/store/ActionCreator.ts
@@ -30,6 +30,7 @@ import { LongRunningOperationApi } from 'api/longRunningOperation';
import { IPipeline, IPushResponse, IRepositoryPipeline, IOperationRun } from '../types';
import { SUPPORT } from 'components/StatusButton/constants';
import { compareTimeInstant } from '../helpers';
+import { getCurrentNamespace } from 'services/NamespaceStore';
const PREFIX = 'features.SourceControlManagement';
@@ -46,6 +47,7 @@ export const getNamespacePipelineList = (namespace, nameFilter = null) => {
return {
name: pipeline.name,
fileHash: pipeline.sourceControlMeta?.fileHash,
+ lastSyncDate: pipeline.sourceControlMeta?.lastSyncedAt,
error: null,
status: null,
};
@@ -432,3 +434,8 @@ export const stopOperation = (namespace: string, operation: IOperationRun) => ()
// The operation status will be updated by the ongoing poll for the operation.
});
};
+
+export const refetchAllPipelines = () => {
+ getNamespacePipelineList(getCurrentNamespace());
+ getRemotePipelineList(getCurrentNamespace());
+};
diff --git a/app/cdap/components/SourceControlManagement/types.ts b/app/cdap/components/SourceControlManagement/types.ts
index 952e80bdcbe..c1b17e4324b 100644
--- a/app/cdap/components/SourceControlManagement/types.ts
+++ b/app/cdap/components/SourceControlManagement/types.ts
@@ -18,23 +18,20 @@ import { IArtifactObj } from 'components/PipelineContextMenu/PipelineTypes';
import { SUPPORT } from 'components/StatusButton/constants';
import { OperationType } from './OperationType';
import { OperationStatus } from './OperationStatus';
+import { ITimeInstant } from 'services/DataFormatter';
export interface IRepositoryPipeline {
name: string;
fileHash: string;
error: string;
status: SUPPORT;
+ lastSyncDate?: ITimeInstant;
}
export interface IOperationResource {
resourceUri: string;
}
-export interface ITimeInstant {
- seconds: number;
- nanos: number;
-}
-
export interface IOperationMeta {
resources: IOperationResource[];
createTime?: ITimeInstant;
@@ -51,6 +48,11 @@ export interface IOperationResourceScopedError {
message?: string;
}
+export interface IOperationResourceScopedErrorMessage {
+ type?: string;
+ message: string;
+}
+
export interface IOperationRun {
id: string;
type: OperationType;
@@ -79,6 +81,7 @@ export interface IPipeline {
};
sourceControlMeta: {
fileHash: string;
+ lastSyncedAt?: ITimeInstant;
};
}
diff --git a/app/cdap/services/DataFormatter/index.ts b/app/cdap/services/DataFormatter/index.ts
index c523d56d5a8..545fb5b0848 100644
--- a/app/cdap/services/DataFormatter/index.ts
+++ b/app/cdap/services/DataFormatter/index.ts
@@ -17,6 +17,11 @@
import moment from 'moment';
import { convertBytesToHumanReadable, humanReadableNumber, truncateNumber } from 'services/helpers';
+export interface ITimeInstant {
+ seconds: number;
+ nanos?: number;
+}
+
export const TYPES = {
STRING: 'STRING',
NUMBER: 'NUMBER',
@@ -117,3 +122,11 @@ export function format(value, type, options: { concise?: boolean } = {}) {
export function formatAsPercentage(str: string) {
return `${str}%`;
}
+
+export function timeInstantToString(t?: ITimeInstant): string {
+ if (!t) {
+ return EMPTY_DATE;
+ }
+
+ return moment(t.seconds * 1000).format('DD-MM-YYYY HH:mm:ss A');
+}
diff --git a/app/cdap/text/text-en.yaml b/app/cdap/text/text-en.yaml
index d2db1f1b5a9..83fa93a91bf 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 {startTime}
+ operationRanFor: Started at {startTime}, Completed in {timeTaken}
+ operationRunningFor: Started at {startTime}, Running for {timeTaken}
pull:
emptyPipelineListMessage: There are no pipelines in the remote repository or no pipelines matching the search query "{query}"
modalTitle: Pull pipeline from remote repository
@@ -3247,12 +3251,12 @@ features:
pipelinesSelected: "{selected} of {total} pipelines selected"
pullAppMessage: Pulling {appId} from remote repository now...
pullAppMessageMulti: Pulling {n} pipelines from the remote repository now...
- pullButton: Pull to namespace
+ pullButton: Pull from repository
pullChipTooltip: Click to pull the latest from Git
pullSuccess: Pipeline {pipelineName} updated
pullSuccessMulti: Successfully pulled {n} pipelines from the remote repository.
pullFailureMulti: Failed to pull {n} pipelines from the remote repository.
- tab: Remote pipelines
+ tab: Repository pipelines
upToDate: Pipeline is already up to date.
stopOperation: STOP
push:
@@ -3268,16 +3272,17 @@ features:
pipelinesSelected: "{selected} of {total} pipelines selected"
pushAppMessage: Pushing {appId} to remote repository now...
pushAppMessageMulti: Pushing {n} pipelines to the remote repository now...
- pushButton: Push to remote
+ pushButton: Push to repository
pushSuccess: Successfully pushed pipeline {pipelineName}
pushSuccessMulti: Successfully pushed {n} pipelines to the remote repository.
pushFailureMulti: Failed to push {n} pipelines to the remote repository.
searchLabel: Search by batch pipeline name
- tab: Local pipelines
+ tab: Namespace pipelines
stopOperation: STOP
table:
connected: Connected
pipelineName: Pipeline name
+ lastSyncDate: Last sync date
gitStatus: Connected to Git
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.