From f17c7e01f50024569cfb06859cf1a61e7f296e0d Mon Sep 17 00:00:00 2001 From: Dunemask Date: Wed, 10 Aug 2022 12:39:09 +0000 Subject: [PATCH] Linked pipeline autoselect --- ROADMAP.md | 2 +- lib/database/queries/catalog.js | 73 +++++++++--- lib/database/queries/results.js | 10 +- src/Queries.jsx | 164 +++++++++++++++++++++++++- src/ctx/JobContext.jsx | 4 +- src/views/catalog/CatalogBox.jsx | 25 +++- src/views/failing/Failing.jsx | 24 +++- src/views/failing/FailingBox.jsx | 49 ++++++-- src/views/jobs/JobPipelineDisplay.jsx | 1 + 9 files changed, 312 insertions(+), 40 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 96ed771..d7eb0be 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,7 @@ - [x] Initial Skeleton - [x] Frontend Drafts -- [ ] Frontend Core +- [x] Frontend Core - [ ] Frontend Pages ## v0.0.2 diff --git a/lib/database/queries/catalog.js b/lib/database/queries/catalog.js index d61778a..972a34b 100644 --- a/lib/database/queries/catalog.js +++ b/lib/database/queries/catalog.js @@ -17,7 +17,7 @@ const testsMock = () => { name: "single", class: "single.js", image: "node:latest", - isCompound: false, + isPipeline: false, type: "api", description: "This is a single test", tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "skip_alt"], @@ -30,7 +30,7 @@ const testsMock = () => { name: "failing", class: "failing.js", image: "node:latest", - isCompound: false, + isPipeline: false, type: "ui", description: "This is a failing test", tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"], @@ -43,7 +43,7 @@ const testsMock = () => { name: "primary", class: "primary.js", image: "node:latest", - isCompound: true, + isPipeline: true, type: "api", description: "This is a primary test", tags: [ @@ -51,7 +51,7 @@ const testsMock = () => { "reg_us", "proj_core", "skip_alt", - "compound_secondary", + "pipeline_secondary1", ], path: "tests/assets/suite/primary.js", created: Date.now(), @@ -59,27 +59,72 @@ const testsMock = () => { }, { id: 3, - name: "secondary", - class: "secondary.js", + name: "secondary1", + class: "secondary1.js", image: "node:latest", - isCompound: true, + isPipeline: true, type: "api", description: "This is a secondary test", - tags: ["cron_1hour", "reg_us", "proj_core", "compound_tertiary"], - path: "tests/assets/suite/secondary.js", + tags: [ + "cron_1hour", + "reg_us", + "proj_core", + "compound_tertiary1", + "compound_tertiary2", + ], + path: "tests/assets/suite/secondary1.js", created: Date.now(), mergeRequest: "https://example.com", }, { id: 4, - name: "tertiary", - class: "tertiary.js", + name: "secondary2", + class: "secondary2.js", image: "node:latest", - isCompound: true, + isPipeline: true, type: "api", - description: "This is a single test", + description: "This is a secondary2 test", + tags: ["cron_1hour", "reg_us", "proj_core", "compound_tertiary3"], + path: "tests/assets/suite/secondary2.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 5, + name: "tertiary1", + class: "tertiary1.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third test", tags: ["cron_1hour", "reg_us", "proj_core"], - path: "tests/assets/suite/tertiary.js", + path: "tests/assets/suite/tertiary1.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 6, + name: "tertiary2", + class: "tertiary2.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third2 test", + tags: ["cron_1hour", "reg_us", "proj_core"], + path: "tests/assets/suite/tertiary2.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 5, + name: "tertiary3", + class: "tertiary3.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third3 test", + tags: ["cron_1hour", "reg_us", "proj_core"], + path: "tests/assets/suite/tertiary3.js", created: Date.now(), mergeRequest: "https://example.com", }, diff --git a/lib/database/queries/results.js b/lib/database/queries/results.js index e4522e3..1a3d47b 100644 --- a/lib/database/queries/results.js +++ b/lib/database/queries/results.js @@ -22,12 +22,12 @@ const failingMock = () => { dailyFails: 12, screenshot: "https://picsum.photos/1920/1080", recentResults: [1, 0, 0, 1, 0], - isCompound: false, + isPipeline: false, failedMessage: `Some Test FailureMessage`, }, { - name: "secondary", - class: "secondary.js", + name: "secondary1", + class: "secondary1.js", timestamp: new Date().toJSON(), method: "FAKEMETHOD", cron: "1hour", @@ -35,8 +35,8 @@ const failingMock = () => { dailyFails: 1, screenshot: "https://picsum.photos/1920/1080", recentResults: [1, 0, 0, 1, 0], - isCompound: true, - failedMessage: `Some Test FailureMessage from Secondary`, + isPipeline: true, + failedMessage: `Some Test FailureMessage from Secondary1`, }, ]; }; diff --git a/src/Queries.jsx b/src/Queries.jsx index 1096963..06e7fd8 100644 --- a/src/Queries.jsx +++ b/src/Queries.jsx @@ -10,7 +10,126 @@ const fetchApi = (subPath) => async () => fetch(`${QUALITEER_URL}${subPath}`).then((res) => res.json()); export const useCatalogTests = () => - useMock ? asMock([]) : useQuery(["catalogTests"], fetchApi("/catalog/tests")); + useMock + ? asMock([ + { + id: 0, + name: "single", + class: "single.js", + image: "node:latest", + isPipeline: false, + type: "api", + description: "This is a single test", + tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "skip_alt"], + path: "tests/assets/suite/single.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 1, + name: "failing", + class: "failing.js", + image: "node:latest", + isPipeline: false, + type: "ui", + description: "This is a failing test", + tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"], + path: "tests/assets/suite/failing.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 2, + name: "primary", + class: "primary.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a primary test", + tags: [ + "cron_1hour", + "reg_us", + "proj_core", + "skip_alt", + "pipeline_secondary1", + ], + path: "tests/assets/suite/primary.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 3, + name: "secondary1", + class: "secondary1.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a secondary test", + tags: [ + "cron_1hour", + "reg_us", + "proj_core", + "compound_tertiary1", + "compound_tertiary2", + ], + path: "tests/assets/suite/secondary1.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 4, + name: "secondary2", + class: "secondary2.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a secondary2 test", + tags: ["cron_1hour", "reg_us", "proj_core", "compound_tertiary3"], + path: "tests/assets/suite/secondary2.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 5, + name: "tertiary1", + class: "tertiary1.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third test", + tags: ["cron_1hour", "reg_us", "proj_core"], + path: "tests/assets/suite/tertiary1.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 6, + name: "tertiary2", + class: "tertiary2.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third2 test", + tags: ["cron_1hour", "reg_us", "proj_core"], + path: "tests/assets/suite/tertiary2.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + { + id: 5, + name: "tertiary3", + class: "tertiary3.js", + image: "node:latest", + isPipeline: true, + type: "api", + description: "This is a third3 test", + tags: ["cron_1hour", "reg_us", "proj_core"], + path: "tests/assets/suite/tertiary3.js", + created: Date.now(), + mergeRequest: "https://example.com", + }, + ]) + : useQuery(["catalogTests"], fetchApi("/catalog/tests")); export const usePipelineMappings = () => useMock @@ -22,7 +141,46 @@ export const usePipelineMappings = () => : useQuery(["pipelineMappings"], fetchApi("/catalog/pipeline-mappings")); export const useSilencedAlerts = () => - useMock ? asMock([]) : useQuery(["silenced"], fetchApi("/alerting/silenced")); + useMock + ? asMock([ + { + name: `failing`, + class: `failing.js`, + method: "FAKEMETHOD", + id: 0, + silencedUntil: new Date().toJSON(), + }, + ]) + : useQuery(["silenced"], fetchApi("/alerting/silenced")); export const useCurrentlyFailing = () => - useMock ? asMock([]) : useQuery(["failing"], fetchApi("/results/failing")); + useMock + ? asMock([ + { + name: "failing", + class: "failing.js", + timestamp: new Date().toJSON(), + method: "FAKEMETHOD", + cron: "1hour", + type: "api", + dailyFails: 12, + screenshot: "https://picsum.photos/1920/1080", + recentResults: [1, 0, 0, 1, 0], + isPipeline: false, + failedMessage: `Some Test FailureMessage`, + }, + { + name: "secondary1", + class: "secondary1.js", + timestamp: new Date().toJSON(), + method: "FAKEMETHOD", + cron: "1hour", + type: "api", + dailyFails: 1, + screenshot: "https://picsum.photos/1920/1080", + recentResults: [1, 0, 0, 1, 0], + isPipeline: true, + failedMessage: `Some Test FailureMessage from Secondary1`, + }, + ]) + : useQuery(["failing"], fetchApi("/results/failing")); diff --git a/src/ctx/JobContext.jsx b/src/ctx/JobContext.jsx index d303b67..9834eb9 100644 --- a/src/ctx/JobContext.jsx +++ b/src/ctx/JobContext.jsx @@ -151,11 +151,13 @@ export const JobProvider = ({ children }) => { const { pipelines } = state; pipelines.push(pipeline); updatePipelines([...pipelines]); - return pipelineComponentJob(pipeline, pipelineReq); + pipelineComponentJob(pipeline, pipelineReq); + return pipeline; } function pipelineCancel(pipelineId) { const pipeline = state.pipelines.find((p) => p.id === pipelineId); + pipeline.isCanceled = true; pipeline.pendingTriggers.forEach((t) => clearTimeout(t)); const jobs = state.jobs.filter( (j) => j.isPipeline && j.pipelineId === pipelineId diff --git a/src/views/catalog/CatalogBox.jsx b/src/views/catalog/CatalogBox.jsx index 0f7b577..e49bf11 100644 --- a/src/views/catalog/CatalogBox.jsx +++ b/src/views/catalog/CatalogBox.jsx @@ -1,5 +1,6 @@ import React, { useState, useContext } from "react"; import { useNavigate } from "react-router-dom"; +import { usePipelineMappings } from "../../Queries.jsx"; import StoreContext from "../../ctx/StoreContext.jsx"; import JobContext from "../../ctx/JobContext.jsx"; @@ -13,6 +14,12 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; +import { + selectBranch, + asTree, + asBranches, + as1d, +} from "../../../lib/jobs/pipelines.js"; export default function CatalogBox(props) { const { catalogTest } = props; @@ -21,12 +28,12 @@ export default function CatalogBox(props) { name: testName, class: testClass, repo: testRepo, - isCompound, + isPipeline, type: testType, } = catalogTest; const navigate = useNavigate(); - + const { data: pipelineMappings, isLoading } = usePipelineMappings(); const { state: store, updateStore } = useContext(StoreContext); const { state: jobState, jobFactory } = useContext(JobContext); @@ -38,10 +45,24 @@ export default function CatalogBox(props) { const runTest = (e) => { e.preventDefault(); e.stopPropagation(); + console.log(catalogTest); + if (isPipeline) return runPipelineTest(); const jobId = jobFactory({ testNames: [testName] }); if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); }; + const runPipelineTest = () => { + const primaries = pipelineMappings.filter((m) => m.includes(testName)); + const builderCache = { + branches: asBranches(primaries), + tree: asTree(primaries), + selectedBranches: as1d(primaries), + isTriage: true, + }; + const pipeline = jobFactory(builderCache); + if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); + }; + function Actions() { return ( diff --git a/src/views/failing/Failing.jsx b/src/views/failing/Failing.jsx index c566844..6b349e3 100644 --- a/src/views/failing/Failing.jsx +++ b/src/views/failing/Failing.jsx @@ -59,16 +59,30 @@ export default function Failing() { const failingTestsWithJobs = () => { const silences = silencedAlerts ?? []; for (var test of failing) { - if (test.isCompound) continue; - const job = jobState.jobs.find((j) => - j.builderCache.testNames.includes(test.name) - ); - if (job) test.job = job; const silence = silences.find( (s) => s.name === test.name || s.class === test.class ); if (silence) test.silencedUntil = silence; + if (test.isPipeline) { + const pipeline = jobState.pipelines.find((p) => + p.selectedBranches.includes(test.name) + ); + if (!pipeline) continue; + const pipelineJob = jobState.jobs.find( + (j) => + j.isPipeline && + j.pipelineId === pipeline.id && + j.branchId === test.name + ); + if (!pipelineJob) test.pipeline = pipeline; + test.job = pipelineJob; + continue; + } + const job = jobState.jobs.find( + (j) => !j.isPipeline && j.builderCache.testNames.includes(test.name) + ); + if (job) test.job = job; } return failing; }; diff --git a/src/views/failing/FailingBox.jsx b/src/views/failing/FailingBox.jsx index f93fd8f..355aeac 100644 --- a/src/views/failing/FailingBox.jsx +++ b/src/views/failing/FailingBox.jsx @@ -1,5 +1,6 @@ import React, { useState, useContext } from "react"; import { useNavigate } from "react-router-dom"; +import { usePipelineMappings } from "../../Queries.jsx"; import StoreContext from "../../ctx/StoreContext.jsx"; import JobContext, { jobStatus } from "../../ctx/JobContext.jsx"; @@ -32,6 +33,13 @@ import Badge from "@mui/material/Badge"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; +import { + selectBranch, + asTree, + asBranches, + as1d, +} from "../../../lib/jobs/pipelines.js"; + const stopPropagation = (e) => e.stopPropagation() && e.preventDefault(); export default function FailingBox(props) { @@ -46,14 +54,15 @@ export default function FailingBox(props) { screenshot: screenshotUrl, recentResults, failedMessage, - isCompound, + isPipeline, jobStatus: testJobStatus, job, + pipeline, } = failingTest; const navigate = useNavigate(); - - const { state: jobState, jobFactory } = useContext(JobContext); + const { data: pipelineMappings, isLoading } = usePipelineMappings(); + const { jobFactory } = useContext(JobContext); const { state: store, updateStore, removeFailure } = useContext(StoreContext); @@ -77,32 +86,54 @@ export default function FailingBox(props) { return "error"; } + const retryPipelineTest = () => { + const primaries = pipelineMappings.filter((m) => m.includes(testName)); + const builderCache = { + branches: asBranches(primaries), + tree: asTree(primaries), + selectedBranches: as1d(primaries), + isTriage: true, + }; + const pipeline = jobFactory(builderCache); + if (store.focusJob) navigate(`/qualiteer/jobs#p${pipeline.id}`); + }; + const retryTest = () => { + if (isPipeline) return retryPipelineTest(); const jobId = jobFactory({ testNames: [testName], isTriage: true }); if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); }; + const navigateToJob = () => { + if (pipeline) return navigate(`/qualiteer/jobs#p${pipeline.id}`); + navigate(`/qualiteer/jobs#${job.jobId}`); + }; + const jobOnClick = () => { + if (pipeline) return navigateToJob; if (!job) return retryTest; switch (job.status) { case jobStatus.OK: - return null; + return navigateToJob; case jobStatus.ERROR: return retryTest; case jobStatus.PENDING: - return null; + return navigateToJob; case jobStatus.ACTIVE: - return null; + return navigateToJob; case jobStatus.CANCELED: - return retryTest; + return navigateToJob; case jobStatus.QUEUED: - return null; + return navigateToJob; default: return retryTest; } }; function jobIcon() { + if (pipeline && pipeline.isCanceled) + return ; + if (pipeline) return ; if (!job) return ; switch (job.status) { case jobStatus.OK: @@ -221,7 +252,7 @@ export default function FailingBox(props) { ) )} - {isCompound && } + {isPipeline && } diff --git a/src/views/jobs/JobPipelineDisplay.jsx b/src/views/jobs/JobPipelineDisplay.jsx index 7a652e0..9046bc6 100644 --- a/src/views/jobs/JobPipelineDisplay.jsx +++ b/src/views/jobs/JobPipelineDisplay.jsx @@ -88,6 +88,7 @@ function JobPipelineDisplay(props) { function jobIcon(name) { const job = findJob(name); const status = job ? job.status : null; + if (pipeline.isCanceled) return ; switch (status) { case jobStatus.OK: return ;