Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GitOps application details page #6137

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,20 +1,118 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { match as Rmatch } from 'react-router-dom';
import { PageHeading } from '@console/internal/components/utils';
import { RouteComponentProps } from 'react-router-dom';
import * as _ from 'lodash';
import { BreadCrumbs, ResourceIcon, LoadingBox } from '@console/internal/components/utils';
import { Split, SplitItem, Label } from '@patternfly/react-core';
import { GitOpsAppGroupData, GitOpsEnvironment } from './utils/gitops-types';
import GitOpsDetailsController from './details/GitOpsDetailsController';
import { routeDecoratorIcon } from '../import/render-utils';
import {
fetchAppGroups,
getEnvData,
getPipelinesBaseURI,
getApplicationsBaseURI,
} from './utils/gitops-utils';
import useDefaultSecret from './utils/useDefaultSecret';

interface GitOpsDetailsPageProps {
match: Rmatch<any>;
}
type GitOpsDetailsPageProps = RouteComponentProps<{ appName?: string }>;

const GitOpsDetailsPage: React.FC<GitOpsDetailsPageProps> = ({ match }) => {
const GitOpsDetailsPage: React.FC<GitOpsDetailsPageProps> = ({ match, location }) => {
const [envs, setEnvs] = React.useState<string[]>(null);
const [envsData, setEnvsData] = React.useState<GitOpsEnvironment[]>(null);
const [secretNS, secretName] = useDefaultSecret();
const { appName } = match.params;
const searchParams = new URLSearchParams(location.search);
const manifestURL = searchParams.get('url');
const pipelinesBaseURI = getPipelinesBaseURI(secretNS, secretName);
const applicationBaseURI = getApplicationsBaseURI(appName, secretNS, secretName, manifestURL);
const environmentBaseURI = `/api/gitops/environments`;

const breadcrumbs = [
{
name: 'GitOps',
path: '/gitops',
},
{
name: 'Application Details',
path: `${match.url}`,
},
];

React.useEffect(() => {
let ignore = false;

const getEnvs = async () => {
if (!pipelinesBaseURI) return;
let appGroups: GitOpsAppGroupData[];
try {
appGroups = await fetchAppGroups(pipelinesBaseURI, manifestURL);
} catch {} // eslint-disable-line no-empty
if (ignore) return;
const app = _.find(appGroups, (appObj) => appName === appObj?.name);
setEnvs(app?.environments);
setEnvs(['dev', 'test', 'qa', 'stage', 'prod']); // will remove this once backend is ready
};

getEnvs();

return () => {
ignore = true;
};
}, [appName, manifestURL, pipelinesBaseURI]);

React.useEffect(() => {
const getEnvsData = async () => {
if (!_.isEmpty(envs) && applicationBaseURI) {
let data;
try {
data = await Promise.all(
_.map(envs, (env) => getEnvData(environmentBaseURI, env, applicationBaseURI)),
);
} catch {} // eslint-disable-line no-empty
setEnvsData(data);
}
};

getEnvsData();
}, [applicationBaseURI, environmentBaseURI, envs, manifestURL]);

return (
<>
<Helmet>
<title>GitOps</title>
</Helmet>
<PageHeading title={appName} kind="application" />
<div className="co-m-nav-title co-m-nav-title--breadcrumbs">
<BreadCrumbs breadcrumbs={breadcrumbs} />
<h1>
<div className="co-m-pane__name co-resource-item">
<ResourceIcon kind="application" className="co-m-resource-icon--lg" />
<span className="co-resource-item__resource-name">{appName}</span>
</div>
</h1>
<Split style={{ alignItems: 'center' }} hasGutter>
<SplitItem style={{ fontWeight: 'bold', fontSize: '12px' }}>
Manifest File Repo:
</SplitItem>
<SplitItem isFilled>
<Label
style={{ fontSize: '12px' }}
divyanshiGupta marked this conversation as resolved.
Show resolved Hide resolved
color="blue"
icon={routeDecoratorIcon(manifestURL, 12)}
>
<a
style={{ color: 'var(--pf-c-label__content--Color)' }}
href={manifestURL}
rel="noopener noreferrer"
target="_blank"
>
{manifestURL}
</a>
</Label>
</SplitItem>
</Split>
</div>
{envsData ? <GitOpsDetailsController envsData={envsData} /> : <LoadingBox />}
</>
);
};
Expand Down
@@ -1,33 +1,21 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import * as _ from 'lodash';
import { PageHeading, LoadingBox } from '@console/internal/components/utils';
import { NamespaceModel } from '@console/internal/models';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { userStateToProps } from '@console/internal/reducers/ui';
import GitOpsList from './GitOpsList';
import { fetchAllAppGroups, getManifestURLs } from './gitops-utils';

interface StateProps {
user: K8sResourceKind;
}
import GitOpsList from './list/GitOpsList';
import { fetchAllAppGroups, getManifestURLs, getPipelinesBaseURI } from './utils/gitops-utils';
import useDefaultSecret from './utils/useDefaultSecret';

const projectRes = { isList: true, kind: NamespaceModel.kind, optional: true };

const GitOpsListPage: React.FC<StateProps> = ({ user }) => {
const GitOpsListPage: React.FC = () => {
const [appGroups, setAppGroups] = React.useState(null);
const [emptyStateMsg, setEmptyStateMsg] = React.useState(null);
const [namespaces, nsLoaded, nsError] = useK8sWatchResource<K8sResourceKind[]>(projectRes);
const userName = _.replace(user?.metadata?.name ?? '', /[^a-zA-Z0-9-]/g, '');
const [secretNS, secretName] =
(userName && [`pipelines-${userName}-github`, `${userName}-github-token`]) || [];
const baseURL =
(secretNS &&
secretName &&
`/api/gitops/pipelines?secretNS=${secretNS}&secretName=${secretName}`) ||
undefined;
const [secretNS, secretName] = useDefaultSecret();
const baseURL = getPipelinesBaseURI(secretNS, secretName);

React.useEffect(() => {
let ignore = false;
Expand Down Expand Up @@ -64,4 +52,4 @@ const GitOpsListPage: React.FC<StateProps> = ({ user }) => {
);
};

export default connect<StateProps, null, null>(userStateToProps)(GitOpsListPage);
export default GitOpsListPage;
@@ -0,0 +1,92 @@
import * as React from 'react';
import * as _ from 'lodash';
// FIXME upgrading redux types is causing many errors at this time
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import { useSelector } from 'react-redux';
import { k8sCreate } from '@console/internal/module/k8s';
import { ImageStreamImportsModel } from '@console/internal/models';
import { LoadingInline, Timestamp } from '@console/internal/components/utils';
import { getActiveNamespace } from '@console/internal/reducers/ui';
import { CommitData } from '../utils/gitops-types';

interface CommitDetailsProps {
imageName: string;
}

const CommitDetails: React.FC<CommitDetailsProps> = ({ imageName }) => {
const [commitData, setCommitData] = React.useState<CommitData>(null);
const namespace = useSelector(getActiveNamespace);
const importImage = {
kind: 'ImageStreamImport',
apiVersion: 'image.openshift.io/v1',
metadata: {
name: 'gitops-app',
namespace,
},
spec: {
import: false,
images: [
{
from: {
kind: 'DockerImage',
name: _.trim(imageName),
},
importPolicy: { insecure: true },
},
],
},
status: {},
};

React.useEffect(() => {
let ignore = false;

const getCommitData = async () => {
let lastCommitData: CommitData = { author: '', timestamp: '', id: '' };
let imageStreamImport;
try {
imageStreamImport = await k8sCreate(ImageStreamImportsModel, importImage);
} catch {} // eslint-disable-line no-empty
if (ignore) return;
const status = imageStreamImport?.status?.images?.[0]?.status;
if (status.status === 'Success') {
const imageLabels =
imageStreamImport?.status?.images?.[0]?.image?.dockerImageMetadata?.Config?.Labels;
lastCommitData = {
author: imageLabels?.['io.openshift.build.commit.author'],
timestamp: imageLabels?.['io.openshift.build.commit.date'],
id: imageLabels?.['io.openshift.build.commit.id'],
};
}
setCommitData(lastCommitData);
};

getCommitData();

return () => {
ignore = true;
};

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (!commitData) return <LoadingInline />;

return (
<>
{commitData.id ? (
<>
<Timestamp timestamp={commitData.timestamp} />
{commitData.id}
{' by '}
{commitData.author}
</>
) : (
<span>Commit details not available</span>
)}
</>
);
};

export default CommitDetails;
@@ -0,0 +1,33 @@
.odc-gitops-details {
padding: var(--pf-global--spacer--xl);
height: 100%;
flex-wrap: nowrap;
display: flex;
&__env-section {
margin-right: 35px;
min-width: 280px;
&__header {
display: flex;
flex-direction: column;
font-size: 12px;
}
&__title {
font-size: 24px;
font-weight: bold;
}
&__timestamp {
font-size: 12px;
color: var(--pf-global--palette--black-600);
}
&__url {
display: inline-flex;
align-items: center;
}
article {
border: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100);
}
a {
font-size: 12px;
}
}
}
@@ -0,0 +1,57 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Stack, StackItem, Card, CardTitle, SplitItem, Split, Label } from '@patternfly/react-core';
import { ResourceLink, ExternalLink } from '@console/internal/components/utils';
import GitOpsServiceDetailsSection from './GitOpsServiceDetailsSection';
import { GitOpsEnvironment } from '../utils/gitops-types';
import './GitOpsDetails.scss';

interface GitOpsDetailsProps {
envs: GitOpsEnvironment[];
}

const GitOpsDetails: React.FC<GitOpsDetailsProps> = ({ envs }) => {
return (
<div className="odc-gitops-details">
{_.map(envs, (env) => (
<Stack className="odc-gitops-details__env-section" key={env.environment}>
<StackItem>
<Card>
<CardTitle className="odc-gitops-details__env-section__header">
<Stack hasGutter>
<StackItem>
<Split style={{ alignItems: 'center' }}>
<SplitItem className="odc-gitops-details__env-section__title" isFilled>
{env.environment}
</SplitItem>
<SplitItem>
<Label className="odc-gitops-details__env-section__timestamp" color="grey">
<span style={{ color: 'var(--pf-global--palette--black-600)' }}>
{env?.timestamp}
</span>
</Label>
</SplitItem>
</Split>
</StackItem>
<StackItem className="co-truncate co-nowrap">
<ExternalLink
additionalClassName="odc-gitops-details__env-section__url"
href={env?.cluster}
text={env?.cluster}
/>
</StackItem>
<StackItem>
<ResourceLink kind="Project" name={env.environment} />
</StackItem>
</Stack>
</CardTitle>
</Card>
</StackItem>
<GitOpsServiceDetailsSection services={env?.services} />
</Stack>
))}
</div>
);
};

export default GitOpsDetails;