RepositoryOverview: Adjust layout and extract necessary components out (#112190)

* RepositoryOverview: adjust layout and extract necessary components out

* clean up

* clean up
This commit is contained in:
Yunwen Zheng
2025-10-09 09:20:29 -04:00
committed by GitHub
parent 66dddb415e
commit caaccb7984
8 changed files with 317 additions and 206 deletions
@@ -3,11 +3,12 @@ import { useMemo } from 'react';
import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { Alert, Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui';
import { Job, Repository, SyncStatus } from 'app/api/clients/provisioning/v0alpha1';
import { Job, Repository } from 'app/api/clients/provisioning/v0alpha1';
import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable';
import { ProvisioningAlert } from '../Shared/ProvisioningAlert';
import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs';
import { getStatusColor } from '../utils/repositoryStatus';
import { formatTimestamp } from '../utils/time';
import { JobSummary } from './JobSummary';
@@ -22,24 +23,12 @@ type JobCell = {
};
};
const getStatusColor = (state?: SyncStatus['state']) => {
switch (state) {
case 'success':
return 'green';
case 'working':
return 'blue';
case 'warning':
return 'orange';
case 'pending':
return 'darkgrey';
case 'error':
return 'red';
default:
return 'darkgrey';
}
};
const getJobColumns = () => [
{
id: 'jobId',
header: t('provisioning.recent-jobs.column-job-id', 'Job ID'),
cell: ({ row: { original: job } }: JobCell) => <Text variant="body">{job.metadata?.name || ''}</Text>,
},
{
id: 'status',
header: t('provisioning.recent-jobs.column-status', 'Status'),
@@ -1,42 +0,0 @@
import { Trans, t } from '@grafana/i18n';
import { Stack, Alert, Text } from '@grafana/ui';
import { HealthStatus } from 'app/api/clients/provisioning/v0alpha1';
interface Props {
health: HealthStatus;
}
export function RepositoryHealth({ health }: Props) {
return (
<Stack gap={2} direction="column" alignItems="flex-start">
{health.healthy ? (
<Alert
title={t('provisioning.repository-health.title-repository-is-healthy', 'Repository is healthy')}
severity="success"
style={{ width: '100%' }}
>
<Trans i18nKey="provisioning.repository-health.no-errors-found">No errors found</Trans>
</Alert>
) : (
<Alert
title={t('provisioning.repository-health.title-repository-is-unhealthy', 'Repository is unhealthy')}
severity="warning"
style={{ width: '100%' }}
>
{health.message && health.message.length > 0 && (
<>
<Text>
<Trans i18nKey="provisioning.repository-health.details">Details:</Trans>
</Text>
<ul>
{health.message.map((message) => (
<li key={message}>{message}</li>
))}
</ul>
</>
)}
</Alert>
)}
</Stack>
);
}
@@ -0,0 +1,88 @@
import { css } from '@emotion/css';
import { t, Trans } from '@grafana/i18n';
import { Badge, Card, Grid, Stack, Text, useStyles2 } from '@grafana/ui';
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { formatTimestamp } from '../utils/time';
import { CheckRepository } from './CheckRepository';
export function RepositoryHealthCard({ repo }: { repo: Repository }) {
const styles = useStyles2(getStyles);
const status = repo.status;
return (
<Card noMargin className={styles.card}>
<Card.Heading>
<Trans i18nKey="provisioning.repository-overview.health">Health</Trans>
</Card.Heading>
<Card.Description>
<Grid columns={3} gap={1} alignItems="baseline">
{/* Status */}
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
</Text>
<div className={styles.spanTwo}>
<Badge
color={status?.health?.healthy ? 'green' : 'red'}
text={
status?.health?.healthy
? t('provisioning.repository-overview.healthy', 'Healthy')
: t('provisioning.repository-overview.unhealthy', 'Unhealthy')
}
icon={status?.health?.healthy ? 'check-circle' : 'exclamation-triangle'}
/>
</div>
{/* Checked */}
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.checked">Checked:</Trans>
</Text>
<div className={styles.spanTwo}>
<Text variant="body">{formatTimestamp(status?.health?.checked)}</Text>
</div>
{!!status?.health?.message?.length && (
<>
<div>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
</Text>
</div>
<div>
<Stack gap={1}>
{status.health.message.map((msg, idx) => (
<Text key={idx} variant="body">
{msg}
</Text>
))}
</Stack>
</div>
</>
)}
</Grid>
</Card.Description>
<Card.Actions className={styles.actions}>
<CheckRepository repository={repo} />
</Card.Actions>
</Card>
);
}
const getStyles = () => {
return {
spanTwo: css({
gridColumn: 'span 2',
}),
card: css({
height: '100%',
display: 'flex',
flexDirection: 'column',
}),
actions: css({
marginTop: 'auto',
}),
};
};
@@ -1,25 +1,26 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaEdition } from '@grafana/data/internal';
import { Trans, t } from '@grafana/i18n';
import { Trans } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Box, Card, CellProps, Grid, InteractiveTable, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1';
import { RecentJobs } from '../Job/RecentJobs';
import { MessageList } from '../Shared/MessageList';
import { formatTimestamp } from '../utils/time';
import { CheckRepository } from './CheckRepository';
import { RepositoryHealth } from './RepositoryHealth';
import { SyncRepository } from './SyncRepository';
import { RepositoryHealthCard } from './RepositoryHealthCard';
import { RepositoryPullStatusCard } from './RepositoryPullStatusCard';
type StatCell<T extends keyof ResourceCount = keyof ResourceCount> = CellProps<ResourceCount, ResourceCount[T]>;
function getColumnCount(hasWebhook: boolean): 3 | 4 {
return hasWebhook ? 4 : 3;
function getColumnCount(hasWebhook: boolean): { xxlColumn: 5 | 4; lgColumn: 3 | 2 } {
return {
xxlColumn: hasWebhook ? 5 : 4,
lgColumn: hasWebhook ? 3 : 2,
};
}
export function RepositoryOverview({ repo }: { repo: Repository }) {
@@ -27,7 +28,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
const status = repo.status;
const webhookURL = getWebhookURL(repo);
const columns = getColumnCount(Boolean(repo.status?.webhook));
const { lgColumn, xxlColumn } = getColumnCount(Boolean(repo.status?.webhook));
const resourceColumns = useMemo(
() => [
@@ -53,7 +54,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
return (
<Box padding={2}>
<Stack direction="column" gap={2}>
<Grid columns={{ xs: 1, sm: 2, lg: columns }} gap={2}>
<Grid columns={{ xs: 1, sm: 2, lg: lgColumn, xxl: xxlColumn }} gap={2} alignItems={'flex-start'}>
<div className={styles.cardContainer}>
<Card noMargin className={styles.card}>
<Card.Heading>
@@ -69,144 +70,20 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
) : null}
</Card.Description>
<Card.Actions className={styles.actions}>
<LinkButton fill="outline" size="md" href={getFolderURL(repo)} icon="folder-open">
<LinkButton size="md" href={getFolderURL(repo)} icon="folder-open" variant="secondary">
<Trans i18nKey="provisioning.repository-overview.view-folder">View Folder</Trans>
</LinkButton>
</Card.Actions>
</Card>
</div>
{repo.status?.health && (
<div className={styles.cardContainer}>
<Card noMargin className={styles.card}>
<Card.Heading>
<Trans i18nKey="provisioning.repository-overview.health">Health</Trans>
</Card.Heading>
<Card.Description>
<RepositoryHealth health={repo.status?.health} />
<Grid columns={12} gap={1} alignItems="baseline">
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">
{status?.health?.healthy
? t('provisioning.repository-overview.healthy', 'Healthy')
: t('provisioning.repository-overview.unhealthy', 'Unhealthy')}
</Text>
</div>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.checked">Checked:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">{formatTimestamp(status?.health?.checked)}</Text>
</div>
{!!status?.health?.message?.length && (
<>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Stack gap={1}>
{status.health.message.map((msg, idx) => (
<Text key={idx} variant="body">
{msg}
</Text>
))}
</Stack>
</div>
</>
)}
</Grid>
</Card.Description>
<Card.Actions className={styles.actions}>
<CheckRepository repository={repo} />
</Card.Actions>
</Card>
<RepositoryHealthCard repo={repo} />
</div>
)}
<div className={styles.cardContainer}>
<Card className={styles.card} noMargin>
<Card.Heading>
<Trans i18nKey="provisioning.repository-overview.pull-status">Pull status</Trans>
</Card.Heading>
<Card.Description>
<Grid columns={12} gap={1} alignItems="baseline">
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">{status?.sync.state ?? 'N/A'}</Text>
</div>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.job-id">Job ID:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">{status?.sync.job ?? 'N/A'}</Text>
</div>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.last-ref">Last Ref:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">
{status?.sync.lastRef
? status.sync.lastRef.substring(0, 7)
: t('provisioning.repository-overview.not-available', 'N/A')}
</Text>
</div>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.started">Started:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">{formatTimestamp(status?.sync.started)}</Text>
</div>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.finished">Finished:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<Text variant="body">{formatTimestamp(status?.sync.finished)}</Text>
</div>
{!!status?.sync?.message?.length && (
<>
<div className={styles.labelColumn}>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
</Text>
</div>
<div className={styles.valueColumn}>
<MessageList messages={status.sync.message} variant="body" />
</div>
</>
)}
</Grid>
</Card.Description>
<Card.Actions className={styles.actions}>
<SyncRepository repository={repo} />
</Card.Actions>
</Card>
</div>
{/* Webhook */}
{repo.status?.webhook && (
<div className={styles.cardContainer}>
<Card noMargin className={styles.card}>
@@ -251,6 +128,16 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
</Card>
</div>
)}
{/* Pull status */}
<div
className={cx(
styles.pullStatusCard,
repo.status?.webhook ? styles.pullStatusCardLgSpan3 : styles.pullStatusCardLgSpan2
)}
>
<RepositoryPullStatusCard repo={repo} />
</div>
</Grid>
{/* job status is not ready for Cloud yet */}
@@ -292,6 +179,23 @@ const getStyles = (theme: GrafanaTheme2) => {
valueColumn: css({
gridColumn: 'span 9',
}),
pullStatusCard: css({
gridColumn: 'span 2',
[theme.breakpoints.down('lg')]: {
gridColumn: 'span 2',
},
}),
pullStatusCardLgSpan3: css({
[theme.breakpoints.down('xxl')]: {
gridColumn: 'span 3',
},
}),
pullStatusCardLgSpan2: css({
[theme.breakpoints.down('xxl')]: {
gridColumn: 'span 2',
},
}),
};
};
@@ -0,0 +1,99 @@
import { css } from '@emotion/css';
import { t, Trans } from '@grafana/i18n';
import { Badge, Card, Grid, Text, TextLink, useStyles2 } from '@grafana/ui';
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
import { MessageList } from '../Shared/MessageList';
import { getRepoCommitUrl } from '../utils/git';
import { getStatusColor, getStatusIcon } from '../utils/repositoryStatus';
import { formatTimestamp } from '../utils/time';
import { SyncRepository } from './SyncRepository';
export function RepositoryPullStatusCard({ repo }: { repo: Repository }) {
const styles = useStyles2(getStyles);
const status = repo.status;
const statusColor = getStatusColor(status?.sync.state);
const statusIcon = getStatusIcon(status?.sync.state);
const { url: lastCommitUrl, hasUrl } = getRepoCommitUrl(repo.spec, status?.sync.lastRef);
return (
<Card noMargin>
<Card.Heading>
<Trans i18nKey="provisioning.repository-overview.pull-status">Pull status</Trans>
</Card.Heading>
<Card.Description>
<Grid columns={3} gap={1} alignItems="baseline">
{/* Status */}
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
</Text>
<div className={styles.spanTwo}>
<Badge icon={statusIcon} color={statusColor} text={status?.sync.state ?? 'N/A'} />
</div>
{/* Job ID */}
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.job-id">Job ID:</Trans>
</Text>
<div className={styles.spanTwo}>
<Text variant="body">{status?.sync.job ?? 'N/A'}</Text>
</div>
{/* Last Ref */}
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.last-ref">Last Ref:</Trans>
</Text>
<div className={styles.spanTwo}>
{hasUrl && lastCommitUrl ? (
<TextLink href={lastCommitUrl} external>
<Text variant="body">
{status?.sync.lastRef
? status.sync.lastRef.substring(0, 7)
: t('provisioning.repository-overview.not-available', 'N/A')}
</Text>
</TextLink>
) : (
<Text variant="body">
{status?.sync.lastRef
? status.sync.lastRef.substring(0, 7)
: t('provisioning.repository-overview.not-available', 'N/A')}
</Text>
)}
</div>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.finished">Last successful pull:</Trans>
</Text>
<div className={styles.spanTwo}>
<Text variant="body">{formatTimestamp(status?.sync.finished)}</Text>
</div>
{!!status?.sync?.message?.length && (
<>
<Text color="secondary">
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
</Text>
<div className={styles.spanTwo}>
<MessageList messages={status.sync.message} variant="body" />
</div>
</>
)}
</Grid>
</Card.Description>
<Card.Actions>
<SyncRepository repository={repo} />
</Card.Actions>
</Card>
);
}
const getStyles = () => {
return {
spanTwo: css({
gridColumn: 'span 2',
}),
};
};
@@ -70,3 +70,40 @@ export const getRepoHrefForProvider = (spec?: RepositorySpec) => {
export function getHasTokenInstructions(type: RepoType): type is InstructionAvailability {
return type === 'github' || type === 'gitlab' || type === 'bitbucket';
}
export function getRepoCommitUrl(spec?: RepositorySpec, commit?: string) {
let url: string | undefined = undefined;
let hasUrl = false;
if (!spec || !spec.type || !commit) {
return { hasUrl, url };
}
const gitType = spec.type;
// local repositories don't have a URL
if (gitType !== 'local' && commit) {
switch (gitType) {
case 'github':
if (spec.github?.url) {
url = `${spec.github.url}/commit/${commit}`;
hasUrl = true;
}
break;
case 'gitlab':
if (spec.gitlab?.url) {
url = `${spec.gitlab.url}/-/commit/${commit}`;
hasUrl = true;
}
break;
case 'bitbucket':
if (spec.bitbucket?.url) {
url = `${spec.bitbucket.url}/commits/${commit}`;
hasUrl = true;
}
break;
}
}
return { hasUrl, url };
}
@@ -0,0 +1,42 @@
import { BadgeColor, IconName } from '@grafana/ui';
import { SyncStatus } from 'app/api/clients/provisioning/v0alpha1';
export interface RepositoryStatus {
color: BadgeColor;
text: string;
icon: IconName;
tooltip?: string;
}
export const getStatusColor = (state?: SyncStatus['state']) => {
switch (state) {
case 'success':
return 'green';
case 'working':
return 'blue';
case 'warning':
return 'orange';
case 'pending':
return 'darkgrey';
case 'error':
return 'red';
default:
return 'darkgrey';
}
};
export const getStatusIcon = (state?: SyncStatus['state']): IconName => {
switch (state) {
case 'success':
return 'check';
case 'working':
case 'warning':
return 'exclamation-triangle';
case 'pending':
return 'spinner';
case 'error':
return 'exclamation-triangle';
default:
return 'exclamation-triangle';
}
};
+2 -8
View File
@@ -11584,6 +11584,7 @@
"active-jobs": "active jobs",
"column-action": "Action",
"column-duration": "Duration",
"column-job-id": "Job ID",
"column-message": "Message",
"column-started": "Started",
"column-status": "Status",
@@ -11602,12 +11603,6 @@
"settings": "Settings",
"view": "View"
},
"repository-health": {
"details": "Details:",
"no-errors-found": "No errors found",
"title-repository-is-healthy": "Repository is healthy",
"title-repository-is-unhealthy": "Repository is unhealthy"
},
"repository-link": {
"delete-or-move-job": {
"compare-branch": "Compare branch",
@@ -11622,7 +11617,7 @@
},
"repository-overview": {
"checked": "Checked:",
"finished": "Finished:",
"finished": "Last successful pull:",
"health": "Health",
"healthy": "Healthy",
"job-id": "Job ID:",
@@ -11631,7 +11626,6 @@
"not-available": "N/A",
"pull-status": "Pull status",
"resources": "Resources",
"started": "Started:",
"status": "Status:",
"unhealthy": "Unhealthy",
"view-folder": "View Folder",