diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 72cdfbc40f..97e2ebe0d1 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3695,6 +3695,7 @@ "actions.runs.delete.description": "Are you sure you want to permanently delete this workflow run? This action cannot be undone.", "actions.runs.not_done": "This workflow run is not done.", "actions.runs.view_workflow_file": "View workflow file", + "actions.runs.workflow_graph": "Workflow Graph", "actions.workflow.disable": "Disable Workflow", "actions.workflow.disable_success": "Workflow '%s' disabled successfully.", "actions.workflow.enable": "Enable Workflow", diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 90c6f99d5e..7a71dad804 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -12,6 +12,7 @@ import ( "time" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo/actions" @@ -58,8 +59,8 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt } func MockActionsView(ctx *context.Context) { - ctx.Data["RunID"] = ctx.PathParam("run") - ctx.Data["JobID"] = ctx.PathParam("job") + ctx.Data["RunIndex"] = ctx.PathParam("run") + ctx.Data["JobIndex"] = ctx.PathParam("job") ctx.HTML(http.StatusOK, "devtest/repo-action-view") } @@ -69,6 +70,7 @@ func MockActionsRunsJobs(ctx *context.Context) { req := web.GetForm(ctx).(*actions.ViewRequest) resp := &actions.ViewResponse{} resp.State.Run.TitleHTML = `mock run title link` + resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) resp.State.Run.Status = actions_model.StatusRunning.String() resp.State.Run.CanCancel = runID == 10 resp.State.Run.CanApprove = runID == 20 @@ -112,6 +114,7 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID * 10, + JobID: "job-100", Name: "job 100", Status: actions_model.StatusRunning.String(), CanRerun: true, @@ -119,17 +122,21 @@ func MockActionsRunsJobs(ctx *context.Context) { }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 1, + JobID: "job-101", Name: "job 101", Status: actions_model.StatusWaiting.String(), CanRerun: false, Duration: "2h", + Needs: []string{"job-100"}, }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID*10 + 2, + JobID: "job-102", Name: "job 102", Status: actions_model.StatusFailure.String(), CanRerun: false, Duration: "3h", + Needs: []string{"job-100", "job-101"}, }) var mockLogOptions []generateMockStepsLogOptions diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 2efcfc84fa..33c1e73aa4 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -143,11 +143,13 @@ type ViewResponse struct { } type ViewJob struct { - ID int64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - CanRerun bool `json:"canRerun"` - Duration string `json:"duration"` + ID int64 `json:"id"` + JobID string `json:"jobId,omitempty"` + Name string `json:"name"` + Status string `json:"status"` + CanRerun bool `json:"canRerun"` + Duration string `json:"duration"` + Needs []string `json:"needs,omitempty"` } type ViewCommit struct { @@ -248,10 +250,12 @@ func ViewPost(ctx *context_module.Context) { for _, v := range jobs { resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{ ID: v.ID, + JobID: v.JobID, Name: v.Name, Status: v.Status.String(), CanRerun: resp.State.Run.CanRerun, Duration: v.Duration().String(), + Needs: v.Needs, }) } diff --git a/routers/web/web.go b/routers/web/web.go index 9e6354e138..b1b31a7ec9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1675,7 +1675,7 @@ func registerWebRoutes(m *web.Router) { m.Any("/mail-preview", devtest.MailPreview) m.Any("/mail-preview/*", devtest.MailPreviewRender) m.Any("/{sub}", devtest.TmplCommon) - m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView) + m.Get("/repo-action-view/runs/{run}/jobs/{job}", devtest.MockActionsView) m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) }) } diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index 677eccc062..292c84c928 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -1,13 +1,13 @@ {{template "base/head" .}}
- Run:CanCancel - Run:CanApprove - Run:CanRerun + Run:CanCancel + Run:CanApprove + Run:CanRerun
{{template "repo/actions/view_component" (dict - "RunIndex" (or .RunID 10) - "JobIndex" (or .JobID 100) + "RunIndex" (or .RunIndex 10) + "JobIndex" (or .JobIndex 0) "ActionsURL" (print AppSubUrl "/devtest/actions-mock") )}}
diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 4e338ffcfc..ebe5158c8a 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -10,6 +10,7 @@ data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" + data-locale-runs-workflow-graph="{{ctx.Locale.Tr "actions.runs.workflow_graph"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 1969b52ed1..8766bf7abc 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -66,14 +66,14 @@ gitea-theme-meta-info { --color-secondary-hover: var(--color-secondary-dark-5); --color-secondary-active: var(--color-secondary-dark-6); /* console colors - used for actions console and console files */ - --color-console-fg: #f7f8f9; - --color-console-fg-subtle: #bdc4cc; - --color-console-bg: #171b1e; - --color-console-border: #2e353b; - --color-console-hover-bg: #272d33; - --color-console-active-bg: #2e353b; - --color-console-menu-bg: #262b31; - --color-console-menu-border: #414b55; + --color-console-fg: #0d1117; + --color-console-fg-subtle: #40474d; + --color-console-bg: #ffffff; + --color-console-border: #d0d7de; + --color-console-hover-bg: #f1f3f5; + --color-console-active-bg: #d0d7de; + --color-console-menu-bg: #f8f9fb; + --color-console-menu-border: #d0d7de; /* named colors */ --color-red: #db2828; --color-orange: #f2711c; diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index b43d025b2d..efa3472e8c 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -8,10 +8,9 @@ import {renderAnsi} from '../render/ansi.ts'; import {POST, DELETE} from '../modules/fetch.ts'; import type {IntervalId} from '../types.ts'; import {toggleFullScreen} from '../utils.ts'; +import WorkflowGraph from './WorkflowGraph.vue' import {localUserSettings} from '../modules/user-settings.ts'; - -// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" -type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; +import type {ActionsRunStatus, ActionsJob} from '../modules/gitea-actions.ts'; type StepContainerElement = HTMLElement & { // To remember the last active logs container, for example: a batch of logs only starts a group but doesn't end it, @@ -54,19 +53,10 @@ const LogLinePrefixCommandMap: Record = { '::remove-matcher': 'hidden', // it has arguments }; - -type Job = { - id: number; - name: string; - status: RunStatus; - canRerun: boolean; - duration: string; -} - type Step = { summary: string, duration: string, - status: RunStatus, + status: ActionsRunStatus, } type JobStepState = { @@ -107,6 +97,7 @@ function isLogElementInViewport(el: Element, {extraViewPortHeight}={extraViewPor type LocaleStorageOptions = { autoScroll: boolean; expandRunning: boolean; + showWorkflowGraph: boolean; actionsLogShowSeconds: boolean; actionsLogShowTimestamps: boolean; }; @@ -116,20 +107,12 @@ export default defineComponent({ components: { SvgIcon, ActionRunStatus, + WorkflowGraph, }, props: { - runIndex: { - type: String, - default: '', - }, - jobIndex: { - type: String, - default: '', - }, - actionsURL: { - type: String, - default: '', - }, + runIndex: {type: Number, required: true}, + jobIndex: {type: Number, required: true}, + actionsURL: {type: String, required: true}, locale: { type: Object as PropType>, default: null, @@ -137,8 +120,8 @@ export default defineComponent({ }, data() { - const defaultViewOptions: LocaleStorageOptions = {autoScroll: true, expandRunning: false, actionsLogShowSeconds: false, actionsLogShowTimestamps: false}; - const {autoScroll, expandRunning, actionsLogShowSeconds, actionsLogShowTimestamps} = localUserSettings.getJsonObject('actions-view-options', defaultViewOptions); + const defaultViewOptions: LocaleStorageOptions = {autoScroll: true, expandRunning: false, showWorkflowGraph: false, actionsLogShowSeconds: false, actionsLogShowTimestamps: false}; + const {autoScroll, expandRunning, showWorkflowGraph, actionsLogShowSeconds, actionsLogShowTimestamps} = localUserSettings.getJsonObject('actions-view-options', defaultViewOptions); return { // internal state loadingAbortController: null as AbortController | null, @@ -147,6 +130,7 @@ export default defineComponent({ artifacts: [] as Array>, menuVisible: false, isFullScreen: false, + showWorkflowGraph: showWorkflowGraph, timeVisible: { 'log-time-stamp': actionsLogShowTimestamps, 'log-time-seconds': actionsLogShowSeconds, @@ -159,7 +143,7 @@ export default defineComponent({ link: '', title: '', titleHTML: '', - status: '' as RunStatus, // do not show the status before initialized, otherwise it would show an incorrect "error" icon + status: '' as ActionsRunStatus, // do not show the status before initialized, otherwise it would show an incorrect "error" icon canCancel: false, canApprove: false, canRerun: false, @@ -176,7 +160,7 @@ export default defineComponent({ // canRerun: false, // duration: '', // }, - ] as Array, + ] as Array, commit: { localeCommit: '', localePushedBy: '', @@ -214,6 +198,9 @@ export default defineComponent({ optionAlwaysExpandRunning() { this.saveLocaleStorageOptions(); }, + showWorkflowGraph() { + this.saveLocaleStorageOptions(); + }, }, async mounted() { @@ -258,6 +245,7 @@ export default defineComponent({ const opts: LocaleStorageOptions = { autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning, + showWorkflowGraph: this.showWorkflowGraph, actionsLogShowSeconds: this.timeVisible['log-time-seconds'], actionsLogShowTimestamps: this.timeVisible['log-time-stamp'], }; @@ -456,11 +444,11 @@ export default defineComponent({ } }, - isDone(status: RunStatus) { + isDone(status: ActionsRunStatus) { return ['success', 'skipped', 'failure', 'cancelled'].includes(status); }, - isExpandable(status: RunStatus) { + isExpandable(status: ActionsRunStatus) { return ['success', 'running', 'failure', 'cancelled'].includes(status); }, @@ -514,15 +502,20 @@ export default defineComponent({

- - - +
+ + + + +
{{ run.workflowID }}: @@ -545,7 +538,7 @@ export default defineComponent({
- +
{{ job.name }} @@ -585,6 +578,15 @@ export default defineComponent({
+ +

@@ -673,6 +675,7 @@ export default defineComponent({ .action-info-summary { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; diff --git a/web_src/js/components/WorkflowGraph.vue b/web_src/js/components/WorkflowGraph.vue new file mode 100644 index 0000000000..dc876d9437 --- /dev/null +++ b/web_src/js/components/WorkflowGraph.vue @@ -0,0 +1,971 @@ + + + + + diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts index 671eef7f3e..242dd30016 100644 --- a/web_src/js/features/repo-actions.ts +++ b/web_src/js/features/repo-actions.ts @@ -11,8 +11,8 @@ export function initRepositoryActionView() { if (parentFullHeight) parentFullHeight.classList.add('tw-pb-0'); const view = createApp(RepoActionView, { - runIndex: el.getAttribute('data-run-index'), - jobIndex: el.getAttribute('data-job-index'), + runIndex: parseInt(el.getAttribute('data-run-index')!), + jobIndex: parseInt(el.getAttribute('data-job-index')!), actionsURL: el.getAttribute('data-actions-url'), locale: { approve: el.getAttribute('data-locale-approve'), @@ -22,6 +22,7 @@ export function initRepositoryActionView() { scheduled: el.getAttribute('data-locale-runs-scheduled'), commit: el.getAttribute('data-locale-runs-commit'), pushedBy: el.getAttribute('data-locale-runs-pushed-by'), + workflowGraph: el.getAttribute('data-locale-runs-workflow-graph'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'), areYouSure: el.getAttribute('data-locale-are-you-sure'), artifactExpired: el.getAttribute('data-locale-artifact-expired'), diff --git a/web_src/js/modules/gitea-actions.ts b/web_src/js/modules/gitea-actions.ts new file mode 100644 index 0000000000..5cc3e096ec --- /dev/null +++ b/web_src/js/modules/gitea-actions.ts @@ -0,0 +1,12 @@ +// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" +export type ActionsRunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; + +export type ActionsJob = { + id: number; + jobId: string; + name: string; + status: ActionsRunStatus; + canRerun: boolean; + needs?: string[]; + duration: string; +};