Linked pipeline autoselect

This commit is contained in:
Dunemask 2022-08-10 12:39:09 +00:00
parent 90d9bc3fcc
commit f17c7e01f5
9 changed files with 312 additions and 40 deletions

View file

@ -6,7 +6,7 @@
- [x] Initial Skeleton - [x] Initial Skeleton
- [x] Frontend Drafts - [x] Frontend Drafts
- [ ] Frontend Core - [x] Frontend Core
- [ ] Frontend Pages - [ ] Frontend Pages
## v0.0.2 ## v0.0.2

View file

@ -17,7 +17,7 @@ const testsMock = () => {
name: "single", name: "single",
class: "single.js", class: "single.js",
image: "node:latest", image: "node:latest",
isCompound: false, isPipeline: false,
type: "api", type: "api",
description: "This is a single test", description: "This is a single test",
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "skip_alt"], tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "skip_alt"],
@ -30,7 +30,7 @@ const testsMock = () => {
name: "failing", name: "failing",
class: "failing.js", class: "failing.js",
image: "node:latest", image: "node:latest",
isCompound: false, isPipeline: false,
type: "ui", type: "ui",
description: "This is a failing test", description: "This is a failing test",
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"], tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"],
@ -43,7 +43,7 @@ const testsMock = () => {
name: "primary", name: "primary",
class: "primary.js", class: "primary.js",
image: "node:latest", image: "node:latest",
isCompound: true, isPipeline: true,
type: "api", type: "api",
description: "This is a primary test", description: "This is a primary test",
tags: [ tags: [
@ -51,7 +51,7 @@ const testsMock = () => {
"reg_us", "reg_us",
"proj_core", "proj_core",
"skip_alt", "skip_alt",
"compound_secondary", "pipeline_secondary1",
], ],
path: "tests/assets/suite/primary.js", path: "tests/assets/suite/primary.js",
created: Date.now(), created: Date.now(),
@ -59,27 +59,72 @@ const testsMock = () => {
}, },
{ {
id: 3, id: 3,
name: "secondary", name: "secondary1",
class: "secondary.js", class: "secondary1.js",
image: "node:latest", image: "node:latest",
isCompound: true, isPipeline: true,
type: "api", type: "api",
description: "This is a secondary test", description: "This is a secondary test",
tags: ["cron_1hour", "reg_us", "proj_core", "compound_tertiary"], tags: [
path: "tests/assets/suite/secondary.js", "cron_1hour",
"reg_us",
"proj_core",
"compound_tertiary1",
"compound_tertiary2",
],
path: "tests/assets/suite/secondary1.js",
created: Date.now(), created: Date.now(),
mergeRequest: "https://example.com", mergeRequest: "https://example.com",
}, },
{ {
id: 4, id: 4,
name: "tertiary", name: "secondary2",
class: "tertiary.js", class: "secondary2.js",
image: "node:latest", image: "node:latest",
isCompound: true, isPipeline: true,
type: "api", 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"], 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(), created: Date.now(),
mergeRequest: "https://example.com", mergeRequest: "https://example.com",
}, },

View file

@ -22,12 +22,12 @@ const failingMock = () => {
dailyFails: 12, dailyFails: 12,
screenshot: "https://picsum.photos/1920/1080", screenshot: "https://picsum.photos/1920/1080",
recentResults: [1, 0, 0, 1, 0], recentResults: [1, 0, 0, 1, 0],
isCompound: false, isPipeline: false,
failedMessage: `Some Test FailureMessage`, failedMessage: `Some Test FailureMessage`,
}, },
{ {
name: "secondary", name: "secondary1",
class: "secondary.js", class: "secondary1.js",
timestamp: new Date().toJSON(), timestamp: new Date().toJSON(),
method: "FAKEMETHOD", method: "FAKEMETHOD",
cron: "1hour", cron: "1hour",
@ -35,8 +35,8 @@ const failingMock = () => {
dailyFails: 1, dailyFails: 1,
screenshot: "https://picsum.photos/1920/1080", screenshot: "https://picsum.photos/1920/1080",
recentResults: [1, 0, 0, 1, 0], recentResults: [1, 0, 0, 1, 0],
isCompound: true, isPipeline: true,
failedMessage: `Some Test FailureMessage from Secondary`, failedMessage: `Some Test FailureMessage from Secondary1`,
}, },
]; ];
}; };

View file

@ -10,7 +10,126 @@ const fetchApi = (subPath) => async () =>
fetch(`${QUALITEER_URL}${subPath}`).then((res) => res.json()); fetch(`${QUALITEER_URL}${subPath}`).then((res) => res.json());
export const useCatalogTests = () => 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 = () => export const usePipelineMappings = () =>
useMock useMock
@ -22,7 +141,46 @@ export const usePipelineMappings = () =>
: useQuery(["pipelineMappings"], fetchApi("/catalog/pipeline-mappings")); : useQuery(["pipelineMappings"], fetchApi("/catalog/pipeline-mappings"));
export const useSilencedAlerts = () => 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 = () => 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"));

View file

@ -151,11 +151,13 @@ export const JobProvider = ({ children }) => {
const { pipelines } = state; const { pipelines } = state;
pipelines.push(pipeline); pipelines.push(pipeline);
updatePipelines([...pipelines]); updatePipelines([...pipelines]);
return pipelineComponentJob(pipeline, pipelineReq); pipelineComponentJob(pipeline, pipelineReq);
return pipeline;
} }
function pipelineCancel(pipelineId) { function pipelineCancel(pipelineId) {
const pipeline = state.pipelines.find((p) => p.id === pipelineId); const pipeline = state.pipelines.find((p) => p.id === pipelineId);
pipeline.isCanceled = true;
pipeline.pendingTriggers.forEach((t) => clearTimeout(t)); pipeline.pendingTriggers.forEach((t) => clearTimeout(t));
const jobs = state.jobs.filter( const jobs = state.jobs.filter(
(j) => j.isPipeline && j.pipelineId === pipelineId (j) => j.isPipeline && j.pipelineId === pipelineId

View file

@ -1,5 +1,6 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePipelineMappings } from "../../Queries.jsx";
import StoreContext from "../../ctx/StoreContext.jsx"; import StoreContext from "../../ctx/StoreContext.jsx";
import JobContext from "../../ctx/JobContext.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 Box from "@mui/material/Box";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import {
selectBranch,
asTree,
asBranches,
as1d,
} from "../../../lib/jobs/pipelines.js";
export default function CatalogBox(props) { export default function CatalogBox(props) {
const { catalogTest } = props; const { catalogTest } = props;
@ -21,12 +28,12 @@ export default function CatalogBox(props) {
name: testName, name: testName,
class: testClass, class: testClass,
repo: testRepo, repo: testRepo,
isCompound, isPipeline,
type: testType, type: testType,
} = catalogTest; } = catalogTest;
const navigate = useNavigate(); const navigate = useNavigate();
const { data: pipelineMappings, isLoading } = usePipelineMappings();
const { state: store, updateStore } = useContext(StoreContext); const { state: store, updateStore } = useContext(StoreContext);
const { state: jobState, jobFactory } = useContext(JobContext); const { state: jobState, jobFactory } = useContext(JobContext);
@ -38,10 +45,24 @@ export default function CatalogBox(props) {
const runTest = (e) => { const runTest = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
console.log(catalogTest);
if (isPipeline) return runPipelineTest();
const jobId = jobFactory({ testNames: [testName] }); const jobId = jobFactory({ testNames: [testName] });
if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); 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() { function Actions() {
return ( return (
<React.Fragment> <React.Fragment>

View file

@ -59,16 +59,30 @@ export default function Failing() {
const failingTestsWithJobs = () => { const failingTestsWithJobs = () => {
const silences = silencedAlerts ?? []; const silences = silencedAlerts ?? [];
for (var test of failing) { 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( const silence = silences.find(
(s) => s.name === test.name || s.class === test.class (s) => s.name === test.name || s.class === test.class
); );
if (silence) test.silencedUntil = silence; 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; return failing;
}; };

View file

@ -1,5 +1,6 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePipelineMappings } from "../../Queries.jsx";
import StoreContext from "../../ctx/StoreContext.jsx"; import StoreContext from "../../ctx/StoreContext.jsx";
import JobContext, { jobStatus } from "../../ctx/JobContext.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 Stack from "@mui/material/Stack";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import {
selectBranch,
asTree,
asBranches,
as1d,
} from "../../../lib/jobs/pipelines.js";
const stopPropagation = (e) => e.stopPropagation() && e.preventDefault(); const stopPropagation = (e) => e.stopPropagation() && e.preventDefault();
export default function FailingBox(props) { export default function FailingBox(props) {
@ -46,14 +54,15 @@ export default function FailingBox(props) {
screenshot: screenshotUrl, screenshot: screenshotUrl,
recentResults, recentResults,
failedMessage, failedMessage,
isCompound, isPipeline,
jobStatus: testJobStatus, jobStatus: testJobStatus,
job, job,
pipeline,
} = failingTest; } = failingTest;
const navigate = useNavigate(); const navigate = useNavigate();
const { data: pipelineMappings, isLoading } = usePipelineMappings();
const { state: jobState, jobFactory } = useContext(JobContext); const { jobFactory } = useContext(JobContext);
const { state: store, updateStore, removeFailure } = useContext(StoreContext); const { state: store, updateStore, removeFailure } = useContext(StoreContext);
@ -77,32 +86,54 @@ export default function FailingBox(props) {
return "error"; 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 = () => { const retryTest = () => {
if (isPipeline) return retryPipelineTest();
const jobId = jobFactory({ testNames: [testName], isTriage: true }); const jobId = jobFactory({ testNames: [testName], isTriage: true });
if (store.focusJob) navigate(`/qualiteer/jobs#${jobId}`); 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 = () => { const jobOnClick = () => {
if (pipeline) return navigateToJob;
if (!job) return retryTest; if (!job) return retryTest;
switch (job.status) { switch (job.status) {
case jobStatus.OK: case jobStatus.OK:
return null; return navigateToJob;
case jobStatus.ERROR: case jobStatus.ERROR:
return retryTest; return retryTest;
case jobStatus.PENDING: case jobStatus.PENDING:
return null; return navigateToJob;
case jobStatus.ACTIVE: case jobStatus.ACTIVE:
return null; return navigateToJob;
case jobStatus.CANCELED: case jobStatus.CANCELED:
return retryTest; return navigateToJob;
case jobStatus.QUEUED: case jobStatus.QUEUED:
return null; return navigateToJob;
default: default:
return retryTest; return retryTest;
} }
}; };
function jobIcon() { function jobIcon() {
if (pipeline && pipeline.isCanceled)
return <DoNotDisturbIcon color="warning" />;
if (pipeline) return <ViewColumnIcon color="secondary" />;
if (!job) return <ReplayIcon />; if (!job) return <ReplayIcon />;
switch (job.status) { switch (job.status) {
case jobStatus.OK: case jobStatus.OK:
@ -221,7 +252,7 @@ export default function FailingBox(props) {
) )
)} )}
</span> </span>
{isCompound && <ViewColumnIcon />} {isPipeline && <ViewColumnIcon />}
</div> </div>
</Typography> </Typography>

View file

@ -88,6 +88,7 @@ function JobPipelineDisplay(props) {
function jobIcon(name) { function jobIcon(name) {
const job = findJob(name); const job = findJob(name);
const status = job ? job.status : null; const status = job ? job.status : null;
if (pipeline.isCanceled) return <DoNotDisturbIcon color="warning" />;
switch (status) { switch (status) {
case jobStatus.OK: case jobStatus.OK:
return <CheckIcon color="success" />; return <CheckIcon color="success" />;