Skip to content

Commit

Permalink
GitOps: Add Sync Status and Latest Deployment Time to List Page (751)
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Chong <[email protected]>
  • Loading branch information
keithchong committed May 14, 2021
1 parent b201205 commit 697848e
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 27 deletions.
5 changes: 5 additions & 0 deletions frontend/packages/gitops-plugin/locales/en/gitops-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
"Environments table": "Environments table",
"Application name": "Application name",
"Git repository": "Git repository",
"Environment status": "Environment status",
"Environment": "Environment",
"Last deployment": "Last deployment",
"Synced": "Synced",
"OutOfSync": "OutOfSync",
"Unknown": "Unknown",
"No GitOps manifest URLs found": "No GitOps manifest URLs found",
"No Application groups found": "No Application groups found"
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const GitOpsList: React.FC<GitOpsListProps> = ({ appGroups, emptyStateMsg }) =>
return fuzzyCaseInsensitive(textFilter, name);
});

const hasSyncStatus: boolean =
appGroups?.some(
({ sync_status }) => sync_status /* eslint-disable-line @typescript-eslint/camelcase */,
) || false;
return (
<div className="odc-gitops-list">
{!emptyStateMsg && appGroups ? (
Expand All @@ -35,7 +39,7 @@ const GitOpsList: React.FC<GitOpsListProps> = ({ appGroups, emptyStateMsg }) =>
<Table
data={visibleItems}
aria-label={t('gitops-plugin~Environments table')}
Header={GitOpsTableHeader}
Header={GitOpsTableHeader(hasSyncStatus)}
Row={GitOpsTableRow}
loaded={!emptyStateMsg}
virtualize
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import { Flex, FlexItem, Tooltip } from '@patternfly/react-core';
import {
GreenCheckCircleIcon,
YellowExclamationTriangleIcon,
GrayUnknownIcon,
} from '@console/shared';

interface SyncProps {
tooltip: any[];
count: number;
icon: string;
}

const GitOpsSyncFragment: React.FC<SyncProps> = ({ tooltip, count, icon }) => {
let targetIcon: React.ReactNode;
if (icon === 'check') {
targetIcon = <GreenCheckCircleIcon />;
} else if (icon === 'exclamation') {
targetIcon = <YellowExclamationTriangleIcon />;
} else {
targetIcon = <GrayUnknownIcon />;
}
return (
<Flex flex={{ default: 'flex_1' }}>
<FlexItem>
{count > 0 ? (
<Tooltip isContentLeftAligned content={<div>{tooltip}</div>}>
<div>
{targetIcon} {count}
</div>
</Tooltip>
) : (
<div>
{targetIcon} {count}
</div>
)}
</FlexItem>
</Flex>
);
};

export default GitOpsSyncFragment;
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { sortable } from '@patternfly/react-table';

const tableColumnClasses = [
classNames('pf-m-width-20'), // Application name
classNames('pf-m-width-40'), // Git repository
classNames('pf-m-width-30'), // Git repository
classNames('pf-m-hidden', 'pf-m-visible-on-md', 'pf-m-width-20'), // Environments
classNames('pf-m-hidden', 'pf-m-visible-on-lg', 'pf-m-width-20'), // Last deployment
classNames('pf-m-hidden', 'pf-m-visible-on-lg', 'pf-m-width-30'), // Last deployment
];

const GitOpsTableHeader = () => {
const GitOpsTableHeader = (hasSyncStatus: boolean) => () => {
return [
{
title: i18n.t('gitops-plugin~Application name'),
Expand All @@ -24,7 +24,9 @@ const GitOpsTableHeader = () => {
props: { className: tableColumnClasses[1] },
},
{
title: i18n.t('gitops-plugin~Environments'),
title: hasSyncStatus
? i18n.t('gitops-plugin~Environment status')
: i18n.t('gitops-plugin~Environment'),
sortField: 'environments',
transforms: [sortable],
props: { className: tableColumnClasses[2] },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.odc-gitops-syncStatus {
max-width: '100px'
}
.odc-gitops-lastDeploymentTime {
color: var(--pf-global--palette--black-600);
}
.odc-gitops-tooltip-text {
color: white;
}
123 changes: 103 additions & 20 deletions frontend/packages/gitops-plugin/src/components/list/GitOpsTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,130 @@ import i18n from '@console/internal/i18n';
import * as classNames from 'classnames';
import { GitOpsAppGroupData } from '../utils/gitops-types';
import { RowFunction, TableData, TableRow } from '@console/internal/components/factory';
import { Button } from '@patternfly/react-core';
import { history, ExternalLink } from '@console/internal/components/utils';
import { Flex, FlexItem, Split, SplitItem } from '@patternfly/react-core';
import { ExternalLink, Timestamp } from '@console/internal/components/utils';
import { routeDecoratorIcon } from '@console/dev-console/src/components/import/render-utils';
import GitOpsSyncFragment from './GitOpsSyncFragment';
import {
GreenCheckCircleIcon,
YellowExclamationTriangleIcon,
GrayUnknownIcon,
} from '@console/shared';
import './GitOpsTableRow.scss';
import { Link } from 'react-router-dom';

const tableColumnClasses = [
classNames('pf-m-width-20'), // Application name
classNames('pf-m-width-40'), // Git repository
classNames('pf-m-width-30'), // Git repository
classNames('pf-m-hidden', 'pf-m-visible-on-md', 'pf-m-width-20'), // Environments
classNames('pf-m-hidden', 'pf-m-visible-on-lg', 'pf-m-width-20'), // Last deployment
classNames('pf-m-hidden', 'pf-m-visible-on-lg', 'pf-m-width-30'), // Last deployment
];

const handleClick = (appGroup: GitOpsAppGroupData) => {
history.push(`/environments/${appGroup.name}?url=${appGroup.repo_url}`);
};
const getMatchingEnvs = (envs: string[], desiredStatus: string) => (
acc: string[],
status: string,
idx: number,
): string[] =>
desiredStatus === status
? [...acc, envs[idx]] // 1:1 between a status and an env
: acc;

const GitOpsTableRow: RowFunction<GitOpsAppGroupData> = (props) => {
const { obj: appGroup, index, key, style } = props;
const {
name,
sync_status: syncStatuses = [],
environments: envs,
last_deployed: lastDeployed = [],
repo_url: repoUrl,
} = appGroup;
const t = (tKey) => i18n.t(tKey);
const syncedEnvs: string[] = syncStatuses.reduce(getMatchingEnvs(envs, 'Synced'), []);
const outOfSyncEnvs: string[] = syncStatuses.reduce(getMatchingEnvs(envs, 'OutOfSync'), []);
const unknownEnvs: string[] = syncStatuses.reduce(getMatchingEnvs(envs, 'Unknown'), []);
const latestDeployedTime = lastDeployed.reduce(
(leadingDeployedTime, deployedTime) =>
leadingDeployedTime < deployedTime ? deployedTime : leadingDeployedTime,
'',
);
const latestDeployedEnv = latestDeployedTime
? envs[lastDeployed.indexOf(latestDeployedTime)]
: '';
return (
<TableRow id={index} index={index} trKey={key} style={style}>
<TableData className={tableColumnClasses[0]}>
<Button
onClick={() => handleClick(appGroup)}
aria-label={appGroup.name}
variant="link"
isInline
>
{appGroup.name}
</Button>
<Link to={`/environments/${appGroup.name}?url=${appGroup.repo_url}`} title={name}>
{name}
</Link>
</TableData>
<TableData className={classNames(tableColumnClasses[1])}>
<ExternalLink href={appGroup.repo_url} additionalClassName={'co-break-all'}>
<ExternalLink href={repoUrl} additionalClassName={'co-break-all'}>
<span style={{ marginRight: 'var(--pf-global--spacer--xs)' }}>
{routeDecoratorIcon(appGroup.repo_url, 12, t)}
{routeDecoratorIcon(repoUrl, 12, t)}
</span>
<span style={{ marginRight: 'var(--pf-global--spacer--xs)' }}>{appGroup.repo_url}</span>
<span style={{ marginRight: 'var(--pf-global--spacer--xs)' }}>{repoUrl}</span>
</ExternalLink>
</TableData>
<TableData className={tableColumnClasses[2]}>{appGroup.environments.join(', ')}</TableData>
<TableData className={classNames(tableColumnClasses[2], 'pf-u-text-nowrap')}>
{syncStatuses.length > 0 ? (
<Flex className="odc-gitops-syncStatus">
<GitOpsSyncFragment
tooltip={syncedEnvs.map((env) => (
<Split className="odc-gitops-tooltip-text" hasGutter key={`${name}-${env}`}>
<SplitItem>
<GreenCheckCircleIcon />
</SplitItem>
<SplitItem isFilled>{env}</SplitItem>
<SplitItem>{t('gitops-plugin~Synced')}</SplitItem>
</Split>
))}
count={syncedEnvs.length}
icon="check"
/>
<GitOpsSyncFragment
tooltip={outOfSyncEnvs.map((env) => (
<Split className="odc-gitops-tooltip-text" hasGutter key={`${name}-${env}`}>
<SplitItem>
<YellowExclamationTriangleIcon />
</SplitItem>
<SplitItem isFilled>{env}</SplitItem>
<SplitItem>{t('gitops-plugin~OutOfSync')}</SplitItem>
</Split>
))}
count={outOfSyncEnvs.length}
icon="exclamation"
/>
<GitOpsSyncFragment
tooltip={unknownEnvs.map((env) => (
<Split className="odc-gitops-tooltip-text" hasGutter key={`${name}-${env}`}>
<SplitItem>
<GrayUnknownIcon />
</SplitItem>
<SplitItem isFilled>{env}</SplitItem>
<SplitItem>{t('gitops-plugin~Unknown')}</SplitItem>
</Split>
))}
count={unknownEnvs.length}
icon="unknown"
/>
</Flex>
) : (
<span>{envs.join(', ')}</span>
)}
</TableData>
<TableData className={tableColumnClasses[3]}>
{/* this is just a placeholder until backend changes can go in to get this data */}-
{latestDeployedTime !== '' ? (
<Flex>
<FlexItem className="odc-gitops-lastDeploymentTime" spacer={{ default: 'spacerXs' }}>
<span>
<Timestamp timestamp={latestDeployedTime} />
</span>
</FlexItem>
<FlexItem>{latestDeployedEnv}</FlexItem>
</Flex>
) : (
<span>-</span>
)}
</TableData>
</TableRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface GitOpsAppGroupData {
name: string;
environments: string[];
repo_url?: string;
sync_status?: string[];
last_deployed?: string[];
}

export interface GitOpsManifestData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@ export const getManifestURLs = (namespaces: K8sResourceKind[]): string[] => {
);
};

export const getApplicationsListBaseURI = () => {
return `/api/gitops/applications`;
};

export const fetchAppGroups = async (
baseURL: string,
manifestURL: string,
): Promise<GitOpsAppGroupData[]> => {
let data: GitOpsManifestData;
try {
data = await coFetchJSON(`${baseURL}&url=${manifestURL}`);
} catch {} // eslint-disable-line no-empty
const newListApi = getApplicationsListBaseURI();
data = await coFetchJSON(`${newListApi}?url=${manifestURL}`);
} catch (err) {
try {
data = await coFetchJSON(`${baseURL}&url=${manifestURL}`);
} catch {} // eslint-disable-line no-empty
}
return data?.applications ?? [];
};

Expand Down

0 comments on commit 697848e

Please sign in to comment.