diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index e6265d804b0..660851f7003 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -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) => {job.metadata?.name || ''}, + }, { id: 'status', header: t('provisioning.recent-jobs.column-status', 'Status'), diff --git a/public/app/features/provisioning/Repository/RepositoryHealth.tsx b/public/app/features/provisioning/Repository/RepositoryHealth.tsx deleted file mode 100644 index daac98dbcaa..00000000000 --- a/public/app/features/provisioning/Repository/RepositoryHealth.tsx +++ /dev/null @@ -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 ( - - {health.healthy ? ( - - No errors found - - ) : ( - - {health.message && health.message.length > 0 && ( - <> - - Details: - - - - )} - - )} - - ); -} diff --git a/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx b/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx new file mode 100644 index 00000000000..4c641c85a04 --- /dev/null +++ b/public/app/features/provisioning/Repository/RepositoryHealthCard.tsx @@ -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 ( + + + Health + + + + {/* Status */} + + Status: + + +
+ +
+ + {/* Checked */} + + Checked: + +
+ {formatTimestamp(status?.health?.checked)} +
+ + {!!status?.health?.message?.length && ( + <> +
+ + Messages: + +
+
+ + {status.health.message.map((msg, idx) => ( + + {msg} + + ))} + +
+ + )} +
+
+ + + +
+ ); +} + +const getStyles = () => { + return { + spanTwo: css({ + gridColumn: 'span 2', + }), + card: css({ + height: '100%', + display: 'flex', + flexDirection: 'column', + }), + actions: css({ + marginTop: 'auto', + }), + }; +}; diff --git a/public/app/features/provisioning/Repository/RepositoryOverview.tsx b/public/app/features/provisioning/Repository/RepositoryOverview.tsx index b176cbf712d..252566f4242 100644 --- a/public/app/features/provisioning/Repository/RepositoryOverview.tsx +++ b/public/app/features/provisioning/Repository/RepositoryOverview.tsx @@ -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 = CellProps; -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 ( - +
@@ -69,144 +70,20 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { ) : null} - + View Folder
+ {repo.status?.health && (
- - - Health - - - - -
- - Status: - -
-
- - {status?.health?.healthy - ? t('provisioning.repository-overview.healthy', 'Healthy') - : t('provisioning.repository-overview.unhealthy', 'Unhealthy')} - -
- -
- - Checked: - -
-
- {formatTimestamp(status?.health?.checked)} -
- - {!!status?.health?.message?.length && ( - <> -
- - Messages: - -
-
- - {status.health.message.map((msg, idx) => ( - - {msg} - - ))} - -
- - )} -
-
- - - -
+
)} -
- - - Pull status - - - -
- - Status: - -
-
- {status?.sync.state ?? 'N/A'} -
-
- - Job ID: - -
-
- {status?.sync.job ?? 'N/A'} -
- -
- - Last Ref: - -
-
- - {status?.sync.lastRef - ? status.sync.lastRef.substring(0, 7) - : t('provisioning.repository-overview.not-available', 'N/A')} - -
- -
- - Started: - -
-
- {formatTimestamp(status?.sync.started)} -
- -
- - Finished: - -
-
- {formatTimestamp(status?.sync.finished)} -
- - {!!status?.sync?.message?.length && ( - <> -
- - Messages: - -
-
- -
- - )} -
-
- - - -
-
+ {/* Webhook */} {repo.status?.webhook && (
@@ -251,6 +128,16 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
)} + + {/* Pull status */} +
+ +
{/* 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', + }, + }), }; }; diff --git a/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx b/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx new file mode 100644 index 00000000000..8758f8de911 --- /dev/null +++ b/public/app/features/provisioning/Repository/RepositoryPullStatusCard.tsx @@ -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 ( + + + Pull status + + + + {/* Status */} + + Status: + +
+ +
+ + {/* Job ID */} + + Job ID: + +
+ {status?.sync.job ?? 'N/A'} +
+ + {/* Last Ref */} + + Last Ref: + +
+ {hasUrl && lastCommitUrl ? ( + + + {status?.sync.lastRef + ? status.sync.lastRef.substring(0, 7) + : t('provisioning.repository-overview.not-available', 'N/A')} + + + ) : ( + + {status?.sync.lastRef + ? status.sync.lastRef.substring(0, 7) + : t('provisioning.repository-overview.not-available', 'N/A')} + + )} +
+ + + Last successful pull: + +
+ {formatTimestamp(status?.sync.finished)} +
+ + {!!status?.sync?.message?.length && ( + <> + + Messages: + +
+ +
+ + )} +
+
+ + + +
+ ); +} + +const getStyles = () => { + return { + spanTwo: css({ + gridColumn: 'span 2', + }), + }; +}; diff --git a/public/app/features/provisioning/utils/git.ts b/public/app/features/provisioning/utils/git.ts index a5d14b3cb5a..7bd221851a2 100644 --- a/public/app/features/provisioning/utils/git.ts +++ b/public/app/features/provisioning/utils/git.ts @@ -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 }; +} diff --git a/public/app/features/provisioning/utils/repositoryStatus.ts b/public/app/features/provisioning/utils/repositoryStatus.ts new file mode 100644 index 00000000000..9d899588506 --- /dev/null +++ b/public/app/features/provisioning/utils/repositoryStatus.ts @@ -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'; + } +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5f0f34d651d..629642ee6f9 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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",