Merge branch 'SwitchToLocalDev' into 'master'
Link K8S deps properly See merge request Dunemask/qualiteer!3
This commit is contained in:
commit
945afdfbbe
64 changed files with 4282 additions and 3069 deletions
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"files": {}
|
|
||||||
}
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules/
|
||||||
build/
|
build/
|
||||||
jobs/*
|
jobs/*
|
||||||
qltr-executor
|
qltr-executor
|
||||||
|
.env
|
||||||
|
|
67
.replit
67
.replit
|
@ -1,67 +0,0 @@
|
||||||
# The command that is executed when the run button is clicked.
|
|
||||||
run = ["echo", "run"]
|
|
||||||
|
|
||||||
entrypoint = "index.js"
|
|
||||||
|
|
||||||
[nix]
|
|
||||||
channel = "stable-21_11"
|
|
||||||
|
|
||||||
[env]
|
|
||||||
XDG_CONFIG_HOME = "/home/runner/.config"
|
|
||||||
|
|
||||||
[packager]
|
|
||||||
language = "nodejs"
|
|
||||||
|
|
||||||
[packager.features]
|
|
||||||
packageSearch = true
|
|
||||||
guessImports = true
|
|
||||||
enabledForHosting = false
|
|
||||||
|
|
||||||
[unitTest]
|
|
||||||
language = "nodejs"
|
|
||||||
|
|
||||||
[languages.javascript]
|
|
||||||
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
|
|
||||||
|
|
||||||
[languages.javascript.languageServer]
|
|
||||||
start = [ "typescript-language-server", "--stdio" ]
|
|
||||||
|
|
||||||
[debugger]
|
|
||||||
support = true
|
|
||||||
|
|
||||||
[debugger.interactive]
|
|
||||||
transport = "localhost:0"
|
|
||||||
startCommand = [ "dap-node" ]
|
|
||||||
|
|
||||||
[debugger.interactive.initializeMessage]
|
|
||||||
command = "initialize"
|
|
||||||
type = "request"
|
|
||||||
|
|
||||||
[debugger.interactive.initializeMessage.arguments]
|
|
||||||
clientID = "replit"
|
|
||||||
clientName = "replit.com"
|
|
||||||
columnsStartAt1 = true
|
|
||||||
linesStartAt1 = true
|
|
||||||
locale = "en-us"
|
|
||||||
pathFormat = "path"
|
|
||||||
supportsInvalidatedEvent = true
|
|
||||||
supportsProgressReporting = true
|
|
||||||
supportsRunInTerminalRequest = true
|
|
||||||
supportsVariablePaging = true
|
|
||||||
supportsVariableType = true
|
|
||||||
|
|
||||||
[debugger.interactive.launchMessage]
|
|
||||||
command = "launch"
|
|
||||||
type = "request"
|
|
||||||
|
|
||||||
[debugger.interactive.launchMessage.arguments]
|
|
||||||
args = []
|
|
||||||
console = "externalTerminal"
|
|
||||||
cwd = "."
|
|
||||||
environment = []
|
|
||||||
pauseForSourceMap = false
|
|
||||||
program = "./index.js"
|
|
||||||
request = "launch"
|
|
||||||
sourceMaps = true
|
|
||||||
stopOnEntry = false
|
|
||||||
type = "pwa-node"
|
|
10
Dockerfile
10
Dockerfile
|
@ -1,16 +1,18 @@
|
||||||
FROM node:16
|
FROM node:18
|
||||||
WORKDIR /dunemask/net/qualiteer
|
WORKDIR /dunemask/net/qualiteer
|
||||||
# Copy dependencies
|
# Copy dependencies
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY package-lock.json .
|
COPY package-lock.json .
|
||||||
RUN npm i
|
RUN npm i
|
||||||
# Copy react build resources over
|
# Copy react build resources over
|
||||||
COPY public public
|
COPY public public
|
||||||
|
COPY dist dist
|
||||||
COPY src src
|
COPY src src
|
||||||
COPY lib lib
|
COPY lib lib
|
||||||
COPY index.html .
|
COPY index.html .
|
||||||
RUN npm run build:react
|
COPY executor.config.js .
|
||||||
|
COPY vite.config.js .
|
||||||
|
RUN npm run build:all
|
||||||
# Copy bin over
|
# Copy bin over
|
||||||
COPY bin bin
|
COPY bin bin
|
||||||
CMD ["npm","start"]
|
CMD ["npm","start"]
|
||||||
|
|
||||||
|
|
14
ROADMAP.md
14
ROADMAP.md
|
@ -7,20 +7,20 @@
|
||||||
- [x] Initial Skeleton
|
- [x] Initial Skeleton
|
||||||
- [x] Frontend Drafts
|
- [x] Frontend Drafts
|
||||||
- [x] Frontend Core
|
- [x] Frontend Core
|
||||||
- [ ] Frontend Pages
|
- [x] Frontend Pages
|
||||||
|
|
||||||
## v0.0.2
|
## v0.0.2
|
||||||
|
|
||||||
- [ ] Database Queries (Req PG)
|
- [x] Database Queries (Req PG)
|
||||||
- [ ] Database Tables (Req PG)
|
- [x] Database Tables (Req PG)
|
||||||
- [ ] Backend Routes (Req Database)
|
- [x] Backend Routes (Req Database)
|
||||||
- [ ] Crons
|
- [ ] Crons
|
||||||
|
|
||||||
## v0.0.3
|
## v0.0.3
|
||||||
|
|
||||||
- [ ] Rabbitmq Consumers (Req Database)
|
- [x] Rabbitmq Consumers (Req Database)
|
||||||
- [ ] Alerting
|
- [x] Alerting
|
||||||
- [ ] Silencing
|
- [x] Silencing
|
||||||
|
|
||||||
## v0.0.4
|
## v0.0.4
|
||||||
|
|
||||||
|
|
BIN
bin/executor
BIN
bin/executor
Binary file not shown.
2
dist/app.js
vendored
2
dist/app.js
vendored
|
@ -4,5 +4,3 @@ import Qualiteer from "qualiteer";
|
||||||
|
|
||||||
const qltr = new Qualiteer();
|
const qltr = new Qualiteer();
|
||||||
await qltr.start();
|
await qltr.start();
|
||||||
|
|
||||||
process.env.INTERNAL_EXECUTOR = "true";
|
|
||||||
|
|
2
dist/bundles/qualiteer-executor.mjs
vendored
2
dist/bundles/qualiteer-executor.mjs
vendored
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
||||||
export default function executorConfig(payload){
|
export default function executorConfig(payload) {
|
||||||
return {
|
return {
|
||||||
command: ({command})=> command,
|
command: ({ command }) => command,
|
||||||
url: ({url})=>url ,
|
url: ({ url }) => url,
|
||||||
jobId: ({jobId}) => jobId,
|
jobId: ({ jobId }) => jobId,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,14 @@ import { INFO, OK, logInfo } from "../util/logging.js";
|
||||||
|
|
||||||
// Import Core Modules
|
// Import Core Modules
|
||||||
import buildRoutes from "../routes/router.js";
|
import buildRoutes from "../routes/router.js";
|
||||||
import buildPostgres from "../database/postgres.js";
|
import pg from "../database/postgres.js";
|
||||||
import injectSockets from "../sockets/socket-server.js";
|
import injectSockets from "../sockets/socket-server.js";
|
||||||
import JobManager from "../jobs/JobManager.js";
|
import JobManager from "../jobs/JobManager.js";
|
||||||
import buildRabbiteer from "../rabbit/rabbit-workers.js";
|
import buildRabbiteer from "../rabbit/rabbit-workers.js";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const title = "QLTR";
|
const title = "QLTR";
|
||||||
|
const rabbiteerEnabled = process.env.QUALITEER_RABBITEER_ENABLED !== "false";
|
||||||
const port = process.env.QUALITEER_DEV_PORT ?? 52000;
|
const port = process.env.QUALITEER_DEV_PORT ?? 52000;
|
||||||
|
|
||||||
// Class
|
// Class
|
||||||
|
@ -27,7 +28,7 @@ export default class Qualiteer {
|
||||||
logInfo(fig.textSync(title, "Cyberlarge"));
|
logInfo(fig.textSync(title, "Cyberlarge"));
|
||||||
INFO("INIT", "Initializing...");
|
INFO("INIT", "Initializing...");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.pg = buildPostgres();
|
this.pg = pg;
|
||||||
this.server = http.createServer(this.app);
|
this.server = http.createServer(this.app);
|
||||||
this.sockets = injectSockets(this.server, this.jobs);
|
this.sockets = injectSockets(this.server, this.jobs);
|
||||||
this.routes = buildRoutes(this.pg, this.sockets);
|
this.routes = buildRoutes(this.pg, this.sockets);
|
||||||
|
@ -37,7 +38,8 @@ export default class Qualiteer {
|
||||||
|
|
||||||
async _connect() {
|
async _connect() {
|
||||||
await this.pg.connect();
|
await this.pg.connect();
|
||||||
// await this.rabbiteer.connect();
|
if (!rabbiteerEnabled) return;
|
||||||
|
await this.rabbiteer.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|
23
lib/database/delays.js
Normal file
23
lib/database/delays.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const seconds = 1000;
|
||||||
|
const minutes = 60 * seconds;
|
||||||
|
const hours = 60 * minutes;
|
||||||
|
export const DELAYS = {
|
||||||
|
"1sec": 1 * seconds,
|
||||||
|
"5sec": 5 * seconds,
|
||||||
|
"10sec": 10 * seconds,
|
||||||
|
"30sec": 30 * seconds,
|
||||||
|
"1min": 1 * minutes,
|
||||||
|
"5min": 5 * minutes,
|
||||||
|
"10min": 10 * minutes,
|
||||||
|
"15min": 15 * minutes,
|
||||||
|
"30min": 30 * minutes,
|
||||||
|
"1hour": 1 * hours,
|
||||||
|
"2hour": 2 * hours,
|
||||||
|
"3hour": 3 * hours,
|
||||||
|
"4hour": 4 * hours,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getDelay(delayStr) {
|
||||||
|
if (DELAYS[delayStr]) return DELAYS[delayStr];
|
||||||
|
return 0;
|
||||||
|
}
|
23
lib/database/migrations/1_create_catalog_table.sql
Normal file
23
lib/database/migrations/1_create_catalog_table.sql
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
CREATE SEQUENCE catalog_id_seq;
|
||||||
|
CREATE TABLE catalog (
|
||||||
|
id bigint NOT NULL DEFAULT nextval('catalog_id_seq') PRIMARY KEY,
|
||||||
|
name varchar(255) DEFAULT NULL,
|
||||||
|
class varchar(255) DEFAULT NULL,
|
||||||
|
image varchar(255) DEFAULT NULL,
|
||||||
|
"path" varchar(255) DEFAULT NULL,
|
||||||
|
description varchar(1023) DEFAULT NULL,
|
||||||
|
type varchar(31) DEFAULT NULL,
|
||||||
|
created TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
mr varchar(255) DEFAULT NULL,
|
||||||
|
tags varchar(255)[] DEFAULT NULL,
|
||||||
|
crons varchar(127) DEFAULT NULL,
|
||||||
|
env varchar(31)[] DEFAULT NULL,
|
||||||
|
regions varchar(15)[] DEFAULT NULL,
|
||||||
|
triggers varchar(255)[] DEFAULT NULL,
|
||||||
|
pipeline BOOLEAN DEFAULT FALSE,
|
||||||
|
coverage varchar(255)[] DEFAULT NULL,
|
||||||
|
projects varchar(255)[] DEFAULT NULL,
|
||||||
|
delay varchar(31) DEFAULT NULL,
|
||||||
|
CONSTRAINT unique_name UNIQUE(name)
|
||||||
|
);
|
||||||
|
ALTER SEQUENCE catalog_id_seq OWNED BY catalog.id;
|
|
@ -1,14 +0,0 @@
|
||||||
CREATE SEQUENCE test_results_id_seq;
|
|
||||||
CREATE TABLE test_results (
|
|
||||||
id bigint NOT NULL DEFAULT nextval('test_results_id_seq') PRIMARY KEY,
|
|
||||||
name varchar(255) DEFAULT NULL,
|
|
||||||
"method" varchar(255) DEFAULT NULL,
|
|
||||||
env varchar(31) DEFAULT NULL,
|
|
||||||
"timestamp" TIMESTAMP NOT NULL DEFAULT now(),
|
|
||||||
retry BOOLEAN DEFAULT FALSE,
|
|
||||||
failed BOOLEAN DEFAULT FALSE,
|
|
||||||
failed_message varchar(2047) DEFAULT NULL,
|
|
||||||
screenshot varchar(255) DEFAULT NULL,
|
|
||||||
weblog varchar(255) DEFAULT NULL
|
|
||||||
);
|
|
||||||
ALTER SEQUENCE test_results_id_seq OWNED BY test_results.id;
|
|
|
@ -1,18 +0,0 @@
|
||||||
CREATE SEQUENCE test_catalog_id_seq;
|
|
||||||
CREATE TABLE test_catalog (
|
|
||||||
id bigint NOT NULL DEFAULT nextval('test_catalog_id_seq') PRIMARY KEY,
|
|
||||||
name varchar(255) DEFAULT NULL,
|
|
||||||
class varchar(255) DEFAULT NULL,
|
|
||||||
compound BOOLEAN DEFAULT FALSE,
|
|
||||||
type varchar(31) DEFAULT NULL,
|
|
||||||
markers varchar(255)[] DEFAULT NULL,
|
|
||||||
ignored BOOLEAN DEFAULT FALSE,
|
|
||||||
comment varchar(1023) DEFAULT NULL,
|
|
||||||
coverage varchar(255)[] DEFAULT NULL,
|
|
||||||
env varchar(31)[] DEFAULT NULL,
|
|
||||||
"path" varchar(255) DEFAULT NULL,
|
|
||||||
regions varchar(15)[] DEFAULT NULL,
|
|
||||||
origin varchar(255) DEFAULT NULL,
|
|
||||||
cron varchar(127) DEFAULT NULL
|
|
||||||
);
|
|
||||||
ALTER SEQUENCE test_catalog_id_seq OWNED BY test_catalog.id;
|
|
15
lib/database/migrations/2_create_results_table.sql
Normal file
15
lib/database/migrations/2_create_results_table.sql
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
CREATE SEQUENCE results_id_seq;
|
||||||
|
CREATE TABLE results (
|
||||||
|
id bigint NOT NULL DEFAULT nextval('results_id_seq') PRIMARY KEY,
|
||||||
|
name varchar(255) DEFAULT NULL,
|
||||||
|
class varchar(255) DEFAULT NULL,
|
||||||
|
"method" varchar(255) DEFAULT NULL,
|
||||||
|
env varchar(31) DEFAULT NULL,
|
||||||
|
"timestamp" TIMESTAMP NOT NULL DEFAULT now(),
|
||||||
|
triage BOOLEAN DEFAULT FALSE,
|
||||||
|
failed BOOLEAN DEFAULT FALSE,
|
||||||
|
message varchar(2047) DEFAULT NULL,
|
||||||
|
screenshot varchar(255) DEFAULT NULL,
|
||||||
|
console varchar(255) DEFAULT NULL
|
||||||
|
);
|
||||||
|
ALTER SEQUENCE results_id_seq OWNED BY results.id;
|
9
lib/database/migrations/3_create_alerting_table.sql
Normal file
9
lib/database/migrations/3_create_alerting_table.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE SEQUENCE alerting_id_seq;
|
||||||
|
CREATE TABLE alerting (
|
||||||
|
id bigint NOT NULL DEFAULT nextval('alerting_id_seq') PRIMARY KEY,
|
||||||
|
name varchar(255) DEFAULT NULL,
|
||||||
|
class varchar(255) DEFAULT NULL,
|
||||||
|
"method" varchar(255) DEFAULT NULL,
|
||||||
|
expires TIMESTAMP NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
ALTER SEQUENCE alerting_id_seq OWNED BY alerting.id;
|
|
@ -52,7 +52,8 @@ export const deleteQuery = (table, jsEntry) => {
|
||||||
const conditionals = [];
|
const conditionals = [];
|
||||||
for (var col of cols) {
|
for (var col of cols) {
|
||||||
entry[col] = buildPostgresValue(entry[col]);
|
entry[col] = buildPostgresValue(entry[col]);
|
||||||
conditionals.push(`x.${col}=${entry[col]}`);
|
if (entry[col] === "null") conditionals.push(`x.${col} IS NULL`);
|
||||||
|
else conditionals.push(`x.${col}=${entry[col]}`);
|
||||||
}
|
}
|
||||||
return `DELETE FROM ${table} x WHERE ${conditionals.join(" AND ")}`;
|
return `DELETE FROM ${table} x WHERE ${conditionals.join(" AND ")}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
import { migrate } from "postgres-migrations";
|
import { migrate } from "postgres-migrations";
|
||||||
import createPgp from "pg-promise";
|
import createPgp from "pg-promise";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { INFO, WARN } from "../util/logging.js";
|
import { INFO, WARN, OK, VERB } from "../util/logging.js";
|
||||||
// Environment Variables
|
// Environment Variables
|
||||||
const {
|
const {
|
||||||
POSTGRES_DATABASE: database,
|
QUALITEER_POSTGRES_DATABASE: database,
|
||||||
POSTGRES_DISABLED: pgDisabled,
|
QUALITEER_POSTGRES_ENABLED: pgEnabled,
|
||||||
POSTGRES_HOST: host,
|
QUALITEER_POSTGRES_HOST: host,
|
||||||
POSTGRES_PASSWORD: password,
|
QUALITEER_POSTGRES_PASSWORD: password,
|
||||||
POSTGRES_PORT: port,
|
QUALITEER_POSTGRES_PORT: port,
|
||||||
POSTGRES_USER: user,
|
QUALITEER_POSTGRES_USER: user,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
// Postgres-promise Configuration
|
// Postgres-promise Configuration
|
||||||
|
@ -34,20 +34,26 @@ const migrationsDir = "lib/database/migrations";
|
||||||
const queryMock = (str) => INFO("POSTGRES MOCK", str);
|
const queryMock = (str) => INFO("POSTGRES MOCK", str);
|
||||||
|
|
||||||
const connect = (pg) => async () => {
|
const connect = (pg) => async () => {
|
||||||
if (pgDisabled) {
|
if (pgEnabled === "false") {
|
||||||
WARN("POSTGRES", "Postgres Disabled!");
|
WARN("POSTGRES", "Postgres Disabled!");
|
||||||
return { query: queryMock };
|
return { query: queryMock };
|
||||||
}
|
}
|
||||||
|
VERB("POSTGRES", "Migrating...");
|
||||||
await migrate(dbConfig, migrationsDir);
|
await migrate(dbConfig, migrationsDir);
|
||||||
// Override the global variable DB
|
// Override fake methods
|
||||||
pg = pgp(dbConfig);
|
const pgInstance = pgp(dbConfig);
|
||||||
|
for (var k in pgInstance) pg[k] = pgInstance[k];
|
||||||
|
VERB("POSTGRES", "Migrated Successfully");
|
||||||
await pg.connect();
|
await pg.connect();
|
||||||
OK("POSTGRES", `Connected to database ${database}!`);
|
VERB("POSTGRES", "Postgres connected Successfully!");
|
||||||
|
|
||||||
|
OK("POSTGRES", `Connected to database ${dbConfig.database}!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildPostgres = () => {
|
const buildPostgres = () => {
|
||||||
var pg = { query: queryMock, connect: connect(pg) };
|
var pg = { query: queryMock };
|
||||||
|
pg.connect = connect(pg);
|
||||||
return pg;
|
return pg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildPostgres;
|
export default buildPostgres();
|
||||||
|
|
|
@ -1,18 +1,56 @@
|
||||||
import pg from "../postgres.js";
|
import pg from "../postgres.js";
|
||||||
import { silencedMock } from "../mocks/alerting-mock.js";
|
import { silencedMock } from "../mocks/alerting-mock.js";
|
||||||
|
import moment from "moment";
|
||||||
// Imports
|
// Imports
|
||||||
import {
|
import {
|
||||||
insertQuery,
|
insertQuery,
|
||||||
selectWhereAnyQuery,
|
selectWhereAnyQuery,
|
||||||
updateWhereAnyQuery,
|
updateWhereAnyQuery,
|
||||||
|
deleteQuery,
|
||||||
} from "../pg-query.js";
|
} from "../pg-query.js";
|
||||||
// Constants
|
// Constants
|
||||||
const table = "silenced_tests";
|
const table = "alerting";
|
||||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||||
|
|
||||||
|
export const upsertAlertSilence = async (silence) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
class: className,
|
||||||
|
method,
|
||||||
|
expires: duration,
|
||||||
|
keepExpires,
|
||||||
|
} = silence;
|
||||||
|
const { h, m } = duration;
|
||||||
|
const expires = moment().add(h, "hours").add(m, "minutes").utc().format();
|
||||||
|
const entry = {
|
||||||
|
name,
|
||||||
|
class: className,
|
||||||
|
method,
|
||||||
|
expires: keepExpires ? undefined : expires,
|
||||||
|
};
|
||||||
|
const asUpdate = {};
|
||||||
|
for (var k of Object.keys(entry))
|
||||||
|
asUpdate[k] = entry[k] === "*" ? null : entry[k];
|
||||||
|
var query = id
|
||||||
|
? updateWhereAnyQuery(table, asUpdate, { id })
|
||||||
|
: insertQuery(table, entry);
|
||||||
|
return pg.query(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAlertSilence = async (silence) => {
|
||||||
|
const { id } = silence;
|
||||||
|
const query = deleteQuery(table, { id });
|
||||||
|
return pg.query(query);
|
||||||
|
};
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
export const getSilencedTests = async () => {
|
export const getSilencedTests = async () => {
|
||||||
if (PG_DISABLED) return silencedMock();
|
if (PG_DISABLED) return silencedMock();
|
||||||
const query = `SELECT * from ${table}`;
|
const query = `SELECT * from ${table}`;
|
||||||
return pg.query(query);
|
const silenced = await pg.query(query);
|
||||||
|
silenced.forEach((t, i) => {
|
||||||
|
for (var k of Object.keys(t)) silenced[i][k] = t[k] === null ? "*" : t[k];
|
||||||
|
});
|
||||||
|
return silenced;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,13 @@ import pg from "../postgres.js";
|
||||||
import {
|
import {
|
||||||
insertQuery,
|
insertQuery,
|
||||||
selectWhereAnyQuery,
|
selectWhereAnyQuery,
|
||||||
updateWhereAnyQuery,
|
onConflictUpdate,
|
||||||
} from "../pg-query.js";
|
} from "../pg-query.js";
|
||||||
|
|
||||||
|
import getFilteredTags from "../tags.js";
|
||||||
|
import getDelay from "../delays.js";
|
||||||
// Constants
|
// Constants
|
||||||
const table = "tests";
|
const table = "catalog";
|
||||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||||
import { testsMock, mappingsMock } from "../mocks/catalog-mock.js";
|
import { testsMock, mappingsMock } from "../mocks/catalog-mock.js";
|
||||||
// Queries
|
// Queries
|
||||||
|
@ -19,8 +22,30 @@ export const getTests = async () => {
|
||||||
|
|
||||||
export const getPipelineMappings = async () => {
|
export const getPipelineMappings = async () => {
|
||||||
if (PG_DISABLED) return mappingsMock();
|
if (PG_DISABLED) return mappingsMock();
|
||||||
const query = `SELECT * from ${table}`;
|
const query = `SELECT * from ${table} WHERE pipeline`;
|
||||||
return pg.query(query);
|
const tests = await pg.query(query);
|
||||||
|
const mappings = [];
|
||||||
|
var newTrigger;
|
||||||
|
for (var test of tests) {
|
||||||
|
if (test.triggers) continue;
|
||||||
|
const { name, delay: delayStr } = test;
|
||||||
|
var triggerStack = [{ name, delay: getDelay(delayStr), delayStr }];
|
||||||
|
newTrigger = { name, delayStr };
|
||||||
|
while (
|
||||||
|
(newTrigger = tests.find(
|
||||||
|
(te) => te.triggers && te.triggers.includes(newTrigger.name)
|
||||||
|
)) !== null
|
||||||
|
) {
|
||||||
|
if (!newTrigger) break;
|
||||||
|
triggerStack.push({
|
||||||
|
name: newTrigger.name,
|
||||||
|
delay: getDelay(newTrigger.delay),
|
||||||
|
delayStr: newTrigger.delay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mappings.push(triggerStack.reverse());
|
||||||
|
}
|
||||||
|
return mappings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjects = async () => {
|
export const getProjects = async () => {
|
||||||
|
@ -28,3 +53,47 @@ export const getProjects = async () => {
|
||||||
const tests = testsMock();
|
const tests = testsMock();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const upsertTest = async (test) => {
|
||||||
|
if (PG_DISABLED) return console.log("Would insert test", test);
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
class: className,
|
||||||
|
image,
|
||||||
|
path,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
created,
|
||||||
|
mergeRequest,
|
||||||
|
tags,
|
||||||
|
} = test;
|
||||||
|
|
||||||
|
const filteredTags = getFilteredTags(tags);
|
||||||
|
|
||||||
|
const env =
|
||||||
|
filteredTags.ignore && filteredTags.env
|
||||||
|
? filteredTags.env.filter((e) => !filteredTags.ignore.includes(e))
|
||||||
|
: filteredTags.env;
|
||||||
|
const catalogEntry = {
|
||||||
|
name,
|
||||||
|
class: className,
|
||||||
|
image,
|
||||||
|
path,
|
||||||
|
description: description ? description : null,
|
||||||
|
type,
|
||||||
|
created,
|
||||||
|
mr: mergeRequest,
|
||||||
|
tags,
|
||||||
|
crons: filteredTags.crons,
|
||||||
|
env,
|
||||||
|
regions: filteredTags.regions,
|
||||||
|
triggers: filteredTags.triggers,
|
||||||
|
pipeline: filteredTags.pipeline ? true : false,
|
||||||
|
coverage: filteredTags.coverage,
|
||||||
|
projects: filteredTags.projects,
|
||||||
|
delay: filteredTags.delay ? filteredTags.delay[0] : null,
|
||||||
|
};
|
||||||
|
const query =
|
||||||
|
insertQuery(table, catalogEntry) + onConflictUpdate(["name"], catalogEntry);
|
||||||
|
return await pg.query(query);
|
||||||
|
};
|
||||||
|
|
|
@ -8,41 +8,36 @@ import {
|
||||||
updateWhereAnyQuery,
|
updateWhereAnyQuery,
|
||||||
} from "../pg-query.js";
|
} from "../pg-query.js";
|
||||||
// Constants
|
// Constants
|
||||||
const table = "test_results";
|
const table = "results";
|
||||||
|
const recentResultsMax = 5;
|
||||||
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
const PG_DISABLED = process.env.POSTGRES_DISABLED;
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
export const insertTestResult = (testResult) => {
|
export const insertTestResult = (testResult) => {
|
||||||
const {
|
const {
|
||||||
test_name,
|
name,
|
||||||
test_class,
|
class: className,
|
||||||
test_method,
|
method,
|
||||||
test_path,
|
env,
|
||||||
test_type,
|
timestamp,
|
||||||
test_timestamp,
|
triage,
|
||||||
test_retry,
|
|
||||||
origin,
|
|
||||||
failed,
|
failed,
|
||||||
failed_message,
|
message,
|
||||||
screenshot_url,
|
screenshot,
|
||||||
expected_screenshot_url,
|
console: cs,
|
||||||
weblog_url,
|
|
||||||
} = testResult;
|
} = testResult;
|
||||||
|
|
||||||
var query = insertQuery(table, {
|
var query = insertQuery(table, {
|
||||||
test_name,
|
name,
|
||||||
test_class,
|
class: className,
|
||||||
test_method,
|
method,
|
||||||
test_path,
|
env,
|
||||||
test_type,
|
timestamp,
|
||||||
test_timestamp,
|
triage,
|
||||||
test_retry,
|
|
||||||
origin,
|
|
||||||
failed,
|
failed,
|
||||||
failed_message,
|
message,
|
||||||
screenshot_url,
|
screenshot,
|
||||||
expected_screenshot_url,
|
console: cs,
|
||||||
weblog_url,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
query += "\n RETURNING *";
|
query += "\n RETURNING *";
|
||||||
|
@ -51,19 +46,48 @@ export const insertTestResult = (testResult) => {
|
||||||
|
|
||||||
export const getCurrentlyFailing = async () => {
|
export const getCurrentlyFailing = async () => {
|
||||||
if (PG_DISABLED) return failingMock();
|
if (PG_DISABLED) return failingMock();
|
||||||
/**/
|
/* This can probably be changed into a super query, but perhaps faster/smaller */
|
||||||
const query = `WITH recent as (SELECT * FROM test_results WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND retry)) SELECT * FROM recent WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed;
|
const recent = `SELECT * FROM ${table} WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND triage)`;
|
||||||
`;
|
const slimCatalog = `SELECT name, crons, class, type, pipeline, env AS enabled_env FROM catalog`;
|
||||||
return pg.query(query);
|
const failing = `SELECT * FROM recent INNER JOIN slim_catalog USING(name) WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed`;
|
||||||
|
const applicableFailing = `SELECT name, count(*) as fails FROM recent WHERE recent.name IN (SELECT name FROM failing) GROUP BY name`;
|
||||||
|
/*const runHistory = `SELECT name, timestamp, failed FROM (SELECT *, ROW_NUMBER() OVER(PARTITION BY name ORDER BY timestamp) as n
|
||||||
|
FROM ${table} WHERE name IN (SELECT name FROM failing)) as ord WHERE n <= ${recentResultsMax} ORDER BY name DESC`;*/
|
||||||
|
const runHistory = `SELECT name, timestamp, failed FROM results WHERE NOT triage AND name IN (SELECT name FROM failing) ORDER BY timestamp DESC LIMIT ${recentResultsMax}`;
|
||||||
|
// const recentQuery = pg.query(recent);
|
||||||
|
const failingQuery = pg.query(
|
||||||
|
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}) ${failing}`
|
||||||
|
);
|
||||||
|
const applicableQuery = pg.query(
|
||||||
|
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}), failing as (${failing}) ${applicableFailing}`
|
||||||
|
);
|
||||||
|
const historyQuery = pg.query(
|
||||||
|
`WITH recent as (${recent}), slim_catalog as (${slimCatalog}), failing as (${failing}) ${runHistory}`
|
||||||
|
);
|
||||||
|
|
||||||
/*SELECT * FROM test_results WHERE "timestamp" BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW(); <-- Last 24 hours all runs*/
|
const [currentlyFailing, applicableFails, failHistory] = await Promise.all([
|
||||||
|
failingQuery,
|
||||||
/* SELECT * FROM test_results tr1 WHERE timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW() AND timestamp = (SELECT MAX(timestamp) FROM test_results tr2 WHERE tr1.name = tr2.name); <-- Last 24 hours only most recent
|
applicableQuery,
|
||||||
*/
|
historyQuery,
|
||||||
|
]);
|
||||||
|
for (var i = 0; i < currentlyFailing.length; i++) {
|
||||||
|
currentlyFailing[i].dailyFails = parseInt(
|
||||||
|
applicableFails.find((af) => af.name === currentlyFailing[i].name).fails
|
||||||
|
);
|
||||||
|
currentlyFailing[i].recentResults = [];
|
||||||
|
currentlyFailing[i].enabledEnv = currentlyFailing[i].enabled_env;
|
||||||
|
currentlyFailing[i].isPipeline = currentlyFailing[i].pipeline;
|
||||||
|
delete currentlyFailing[i].enabled_env;
|
||||||
|
delete currentlyFailing[i].pipeline;
|
||||||
|
for (var fh of failHistory) {
|
||||||
|
if (fh.name !== currentlyFailing[i].name) continue;
|
||||||
|
currentlyFailing[i].recentResults.push(fh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentlyFailing;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentlyFailingFull = (env) => {
|
export const ignoreResult = async ({ id }) => {
|
||||||
const query = `WITH recent AS (SELECT * FROM test_results WHERE (timestamp BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()) AND NOT(failed AND retry) AND env='prod') SELECT * FROM recent INNER JOIN test_catalog USING(name) WHERE timestamp = (SELECT MAX(timestamp) FROM recent r2 WHERE recent.name = r2.name) AND failed;
|
const query = updateWhereAnyQuery(table, { failed: false }, { id });
|
||||||
;`;
|
|
||||||
return pg.query(query);
|
return pg.query(query);
|
||||||
};
|
};
|
||||||
|
|
30
lib/database/seed.js
Normal file
30
lib/database/seed.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import pg from "./postgres.js";
|
||||||
|
import { upsertTest } from "./queries/catalog.js";
|
||||||
|
import { insertTestResult } from "./queries/results.js";
|
||||||
|
import { upsertAlertSilence } from "./queries/alerting.js";
|
||||||
|
import {
|
||||||
|
seed as catalogSeed,
|
||||||
|
table as catalogTable,
|
||||||
|
} from "./seeds/catalog-seed.js";
|
||||||
|
import {
|
||||||
|
seed as resultsSeed,
|
||||||
|
table as resultsTable,
|
||||||
|
} from "./seeds/results-seed.js";
|
||||||
|
import {
|
||||||
|
seed as alertingSeed,
|
||||||
|
table as alertingTable,
|
||||||
|
} from "./seeds/alerting-seed.js";
|
||||||
|
|
||||||
|
const database = process.env.POSTGRES_DATABASE ?? "qualiteer";
|
||||||
|
await pg.connect();
|
||||||
|
|
||||||
|
const resetAndSeed = async (table, getSeeds, seed) => {
|
||||||
|
await pg.query(`TRUNCATE ${table} RESTART IDENTITY CASCADE;`);
|
||||||
|
for (var s of getSeeds()) await seed(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
await resetAndSeed(catalogTable, catalogSeed, upsertTest);
|
||||||
|
await resetAndSeed(resultsTable, resultsSeed, insertTestResult);
|
||||||
|
await resetAndSeed(alertingTable, alertingSeed, upsertAlertSilence);
|
||||||
|
|
||||||
|
process.exit();
|
11
lib/database/seeds/alerting-seed.js
Normal file
11
lib/database/seeds/alerting-seed.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const table = "alerting";
|
||||||
|
export const seed = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: `failing`,
|
||||||
|
class: `failing.js`,
|
||||||
|
method: "FAKEMETHOD",
|
||||||
|
expires: new Date().toJSON(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
126
lib/database/seeds/catalog-seed.js
Normal file
126
lib/database/seeds/catalog-seed.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
export const table = "catalog";
|
||||||
|
export const seed = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "single",
|
||||||
|
class: "single.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/single.js",
|
||||||
|
description: "This is a single test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core", "ignore_alt"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "failing",
|
||||||
|
class: "failing.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/failing.js",
|
||||||
|
description: "This is a failing test",
|
||||||
|
type: "ui",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["cron_1hour", "reg_us", "env_ci", "proj_core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "primary",
|
||||||
|
class: "primary.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/primary.js",
|
||||||
|
description: "This is a primary test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: [
|
||||||
|
"pipeline",
|
||||||
|
"cron_1hour",
|
||||||
|
"reg_us",
|
||||||
|
"proj_core",
|
||||||
|
"ignore_alt",
|
||||||
|
"triggers_secondary1",
|
||||||
|
"triggers_secondary2",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary1",
|
||||||
|
class: "secondary1.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/secondary1.js",
|
||||||
|
description: "This is a secondary test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: [
|
||||||
|
"pipeline",
|
||||||
|
"cron_1hour",
|
||||||
|
"reg_us",
|
||||||
|
"proj_core",
|
||||||
|
"triggers_tertiary1",
|
||||||
|
"triggers_tertiary2",
|
||||||
|
"delay_1sec",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary2",
|
||||||
|
class: "secondary2.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/secondary2.js",
|
||||||
|
description: "This is a secondary2 test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: [
|
||||||
|
"pipeline",
|
||||||
|
"cron_1hour",
|
||||||
|
"reg_us",
|
||||||
|
"proj_core",
|
||||||
|
"triggers_tertiary3",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tertiary1",
|
||||||
|
class: "tertiary1.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/tertiary1.js",
|
||||||
|
description: "This is a third test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tertiary2",
|
||||||
|
class: "tertiary2.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/tertiary2.js",
|
||||||
|
description: "This is a third2 test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core", "delay_10sec"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tertiary3",
|
||||||
|
class: "tertiary3.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/tertiary3.js",
|
||||||
|
description: "This is a third3 test",
|
||||||
|
type: "api",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["pipeline", "cron_1hour", "reg_us", "proj_core", "delay_5sec"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single-alt",
|
||||||
|
class: "single-alt.js",
|
||||||
|
image: "node:latest",
|
||||||
|
path: "tests/assets/suite/single-alt.js",
|
||||||
|
description: "This is an alternative test",
|
||||||
|
type: "ui",
|
||||||
|
created: new Date().toJSON(),
|
||||||
|
mergeRequest: "https://example.com",
|
||||||
|
tags: ["cron_1hour", "reg_us", "env_ci", "proj_alt"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
29
lib/database/seeds/results-seed.js
Normal file
29
lib/database/seeds/results-seed.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
export const table = "results";
|
||||||
|
export const seed = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "failing",
|
||||||
|
class: "failing.js",
|
||||||
|
method: "FAKEMETHOD",
|
||||||
|
env: "prod",
|
||||||
|
timestamp: new Date().toJSON(),
|
||||||
|
triage: false,
|
||||||
|
failed: true,
|
||||||
|
message: "Some Test FailureMessage",
|
||||||
|
screenshot: "https://picsum.photos/1920/1080",
|
||||||
|
console: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secondary1",
|
||||||
|
class: "secondary1.js",
|
||||||
|
method: "FAKEMETHOD",
|
||||||
|
env: "prod",
|
||||||
|
timestamp: new Date().toJSON(),
|
||||||
|
triage: false,
|
||||||
|
failed: true,
|
||||||
|
message: "Some Test FailureMessage from Secondary1",
|
||||||
|
screenshot: "https://picsum.photos/1920/1080",
|
||||||
|
console: "https://example.com",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
26
lib/database/tags.js
Normal file
26
lib/database/tags.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { WARN } from "../util/logging.js";
|
||||||
|
export const TAGS = {
|
||||||
|
IGNORE: { name: "ignore", tag: "ignore_", value: (t) => t },
|
||||||
|
CRON: { name: "crons", tag: "cron_", value: (t) => t },
|
||||||
|
ENV: { name: "env", tag: "env_", value: (t) => t },
|
||||||
|
REGIONS: { name: "regions", tag: "reg_", value: (t) => t },
|
||||||
|
PIPELINE: { name: "pipeline", tag: "pipeline", value: (t) => t },
|
||||||
|
COVERAGE: { name: "coverage", tag: "coverage_", value: (t) => t },
|
||||||
|
PROJECT: { name: "projects", tag: "proj_", value: (t) => t },
|
||||||
|
DELAY: { name: "delay", tag: "delay_", value: (t) => t },
|
||||||
|
TRIGGERS: { name: "triggers", tag: "triggers_", value: (t) => t },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getFilteredTags(tags) {
|
||||||
|
const filtered = {};
|
||||||
|
for (var t of tags) {
|
||||||
|
const tag = Object.values(TAGS).find((ta) => t.startsWith(ta.tag));
|
||||||
|
if (!tag) {
|
||||||
|
WARN("CATALOG", `Tag '${t}' did not have a valid prefix!`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!filtered[tag.name]) filtered[tag.name] = [];
|
||||||
|
filtered[tag.name].push(tag.value(t.replace(tag.tag, "")));
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
|
@ -27,6 +27,8 @@ class JobManager {
|
||||||
|
|
||||||
pushLog(jobId, log) {
|
pushLog(jobId, log) {
|
||||||
const job = this.getJobById(jobId);
|
const job = this.getJobById(jobId);
|
||||||
|
if (!job) return;
|
||||||
|
|
||||||
if (log instanceof Array) job.log.push(...log);
|
if (log instanceof Array) job.log.push(...log);
|
||||||
else job.log.push(log);
|
else job.log.push(log);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
const baseCommand = "node";
|
const baseCommand = "node";
|
||||||
const suiteEntry = "tests/assets/suite/runner.js";
|
const suiteEntry = "tests/assets/suite/runner.js";
|
||||||
const pipelineMapping = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
pipeline: [{ name: "primary" }, { name: "secondary", delay: 5000 }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const buildCommon = (jobRequest) => {
|
const buildCommon = (jobRequest) => {
|
||||||
const { isTriage, ignore, region, testNames } = jobRequest;
|
const { isTriage, ignore, region, testNames } = jobRequest;
|
||||||
|
@ -25,7 +19,6 @@ const buildManual = (jobReq) => {
|
||||||
throw Error("Currently only 1 test can be selected!");
|
throw Error("Currently only 1 test can be selected!");
|
||||||
|
|
||||||
command.push(`test=${testNames[0]}`);
|
command.push(`test=${testNames[0]}`);
|
||||||
|
|
||||||
return { ...jobReq, command };
|
return { ...jobReq, command };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,7 +44,6 @@ const buildPipeline = (jobReq, socketId) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function jobBuilder(jobRequest, id) {
|
export default function jobBuilder(jobRequest, id) {
|
||||||
console.log(jobRequest);
|
|
||||||
const jobReq = buildCommon(jobRequest, id);
|
const jobReq = buildCommon(jobRequest, id);
|
||||||
const { pipeline, testNames, tags } = jobReq;
|
const { pipeline, testNames, tags } = jobReq;
|
||||||
if (pipeline) return buildPipeline(jobReq, id);
|
if (pipeline) return buildPipeline(jobReq, id);
|
||||||
|
|
|
@ -5,22 +5,13 @@
|
||||||
"name": "qltr-job-test-suite-1"
|
"name": "qltr-job-test-suite-1"
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
|
"ttlSecondsAfterFinished": 2,
|
||||||
"template": {
|
"template": {
|
||||||
"spec": {
|
"spec": {
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"resources": {
|
|
||||||
"requests": {
|
|
||||||
"memory": "64MI",
|
|
||||||
"cpu": "250m"
|
|
||||||
},
|
|
||||||
"limits": {
|
|
||||||
"memory": "128MI",
|
|
||||||
"cpu": "500m"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "qltr-job-test-suite-1",
|
"name": "qltr-job-test-suite-1",
|
||||||
"image": "node",
|
"image": "node:latest",
|
||||||
"imagePullPolicy": "Always",
|
"imagePullPolicy": "Always",
|
||||||
"command": ["node", "--version"]
|
"command": ["node", "--version"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
import cp from "node:child_process";
|
import k8s from "@kubernetes/client-node";
|
||||||
|
import { INFO, ERR } from "../../util/logging.js";
|
||||||
import { jobBuilder, createFile, deleteFile } from "./k8s-common.js";
|
import { jobBuilder, createFile, deleteFile } from "./k8s-common.js";
|
||||||
|
|
||||||
const applyFile = async (filePath) => {
|
|
||||||
const command = `kubectl apply -f ${filePath}`;
|
|
||||||
return new Promise((res, rej) =>
|
|
||||||
cp.exec(command, (err, stdout, stderr) => (err && rej(err)) || res(stdout))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function createJob(jobRequest) {
|
export default async function createJob(jobRequest) {
|
||||||
const job = jobBuilder(jobRequest);
|
const job = jobBuilder(jobRequest);
|
||||||
const filePath = createFile(job);
|
job.spec.template.spec.containers[0].image = "node:latest";
|
||||||
|
job.spec.template.spec.containers[0].command = ["node", "--version"];
|
||||||
|
// job.spec.template.spec.containers[0].image = "reed";
|
||||||
|
// job.spec.template.spec.containers[0].command = "python3 -m pytest -v --tb=no -p no:warnings".split(" ");
|
||||||
|
const kc = new k8s.KubeConfig();
|
||||||
|
kc.loadFromCluster();
|
||||||
|
const batchV1Api = kc.makeApiClient(k8s.BatchV1Api);
|
||||||
|
const batchV1beta1Api = kc.makeApiClient(k8s.BatchV1beta1Api);
|
||||||
|
const jobName = job.metadata.name;
|
||||||
|
batchV1Api
|
||||||
|
.createNamespacedJob("dunestorm-dunemask", job)
|
||||||
|
.then((res) => INFO("K8S", `Job ${jobName} created!`))
|
||||||
|
.catch((err) => ERR("K8S", err));
|
||||||
|
|
||||||
|
/*const filePath = createFile(job);
|
||||||
applyFile(filePath);
|
applyFile(filePath);
|
||||||
deleteFile(filePath);
|
deleteFile(filePath);*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,21 @@ import Rabbiteer from "rabbiteer";
|
||||||
import buildWorkers from "./workers/index.js";
|
import buildWorkers from "./workers/index.js";
|
||||||
|
|
||||||
// Pull Environment Variables
|
// Pull Environment Variables
|
||||||
const { RABBIT_HOST: host, RABBIT_USER: user, RABBIT_PASS: pass } = process.env;
|
const {
|
||||||
|
QUALITEER_RABBIT_HOST: host,
|
||||||
|
QUALITEER_RABBIT_USER: user,
|
||||||
|
QUALITEER_RABBIT_PASS: pass,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
// Rabbit Config
|
// Rabbit Config
|
||||||
const rabbitConfig = {
|
const rabbitConfig = {
|
||||||
host: host ?? "localhost",
|
protocol: "amqp:",
|
||||||
user: user ?? "rabbit",
|
host: `amqp://${host}` ?? "localhost",
|
||||||
pass: pass ?? "rabbit",
|
user: user ?? "guest",
|
||||||
|
pass: pass ?? "guest",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildRabbiteer = (skio) =>
|
const buildRabbiteer = (pg, skio) =>
|
||||||
new Rabbiteer(null, buildWorkers(skio), { autoRabbit: rabbitConfig });
|
new Rabbiteer(null, buildWorkers(skio), { autoRabbit: rabbitConfig });
|
||||||
|
|
||||||
export default buildRabbiteer;
|
export default buildRabbiteer;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// Imports
|
// Imports
|
||||||
import { Worker } from "rabbiteer";
|
import { Worker } from "rabbiteer";
|
||||||
|
import { VERB } from "../../util/logging.js";
|
||||||
|
import { insertTestResult } from "../../database/queries/results.js";
|
||||||
import evt from "../../sockets/events.js";
|
import evt from "../../sockets/events.js";
|
||||||
// Class
|
// Class
|
||||||
export default class TestResultsWorker extends Worker {
|
export default class TestResultsWorker extends Worker {
|
||||||
|
@ -23,9 +25,9 @@ export default class TestResultsWorker extends Worker {
|
||||||
consoleLogUrl: “https://consolelog”
|
consoleLogUrl: “https://consolelog”
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
onMessage(testResult) {
|
async onMessage(testResult) {
|
||||||
const { pipeline } = testResult;
|
const { pipeline } = testResult;
|
||||||
|
await this.handleReporting(testResult);
|
||||||
// Alter to start next test
|
// Alter to start next test
|
||||||
// TODO the delay should be autopopulated either by the suite, or filled in by the server
|
// TODO the delay should be autopopulated either by the suite, or filled in by the server
|
||||||
if (pipeline) return this.pipelineTrigger(pipeline);
|
if (pipeline) return this.pipelineTrigger(pipeline);
|
||||||
|
@ -35,4 +37,9 @@ export default class TestResultsWorker extends Worker {
|
||||||
const { dashboardSocketId: dsi } = pipeline;
|
const { dashboardSocketId: dsi } = pipeline;
|
||||||
this.skio.to(dsi).emit(evt.PPL_TRG, pipeline);
|
this.skio.to(dsi).emit(evt.PPL_TRG, pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleReporting(result) {
|
||||||
|
VERB("TestResults", result.name);
|
||||||
|
insertTestResult(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { Router, json as jsonMiddleware } from "express";
|
import { Router, json as jsonMiddleware } from "express";
|
||||||
import { getSilencedTests } from "../database/queries/alerting.js";
|
import {
|
||||||
|
getSilencedTests,
|
||||||
|
upsertAlertSilence,
|
||||||
|
deleteAlertSilence,
|
||||||
|
} from "../database/queries/alerting.js";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Apply Middlewares
|
// Apply Middlewares
|
||||||
|
@ -12,7 +16,26 @@ router.get("/silenced", (req, res) => {
|
||||||
|
|
||||||
// Post Routes
|
// Post Routes
|
||||||
router.post("/silence", (req, res) => {
|
router.post("/silence", (req, res) => {
|
||||||
res.sendStatus(200);
|
const { name, class: className, method, expires, keepExpires } = req.body;
|
||||||
|
if (!name || !className || !method)
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send("'name', 'class', and 'method' are all required Fields!");
|
||||||
|
if (expires === null)
|
||||||
|
return deleteAlertSilence(req.body)
|
||||||
|
.then(() => res.sendStatus(200))
|
||||||
|
.catch((e) => res.status(500).send(e));
|
||||||
|
const { h, m } = keepExpires ? {} : expires;
|
||||||
|
if (!keepExpires && (h == null || m == null))
|
||||||
|
return res.status(400).send("Both 'h' and 'm' are required fields!");
|
||||||
|
if (!keepExpires && (h < 0 || m < 0))
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send("'h' and 'm' must be greater than or equal to 0!");
|
||||||
|
// TODO set max times as well
|
||||||
|
if (!keepExpires && (h > 72 || m > 59))
|
||||||
|
res.status(400).send("'h' and 'm' must not exceed the set maxes!");
|
||||||
|
upsertAlertSilence(req.body).then(() => res.sendStatus(200));
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { Router, json as jsonMiddleware } from "express";
|
import { Router, json as jsonMiddleware } from "express";
|
||||||
import { getTests, getPipelineMappings } from "../database/queries/catalog.js";
|
import {
|
||||||
|
getTests,
|
||||||
|
getPipelineMappings,
|
||||||
|
upsertTest,
|
||||||
|
} from "../database/queries/catalog.js";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
const maxSize = 1024 * 1024 * 100; // 100MB
|
const maxSize = 1024 * 1024 * 100; // 100MB
|
||||||
|
@ -18,8 +22,10 @@ router.get("/pipeline-mappings", (req, res) => {
|
||||||
|
|
||||||
// Post Routes
|
// Post Routes
|
||||||
router.post("/update", (req, res) => {
|
router.post("/update", (req, res) => {
|
||||||
// Update All Tests
|
if (!req.body) return res.status(400).send("Body required!");
|
||||||
res.sendStatus(200);
|
if(!Array.isArray(req.body)) return res.status(400).send("Body must be an array!");
|
||||||
|
const upserts = Promise.all(req.body.map((catalogItem)=>upsertTest(catalogItem)));
|
||||||
|
upserts.then(()=>res.sendStatus(200)).catch((e)=>res.status(500).send(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { Router, json as jsonMiddleware } from "express";
|
import { Router, json as jsonMiddleware } from "express";
|
||||||
import { getCurrentlyFailing } from "../database/queries/results.js";
|
import {
|
||||||
|
getCurrentlyFailing,
|
||||||
|
ignoreResult,
|
||||||
|
} from "../database/queries/results.js";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Apply Middlewares
|
// Apply Middlewares
|
||||||
|
@ -15,4 +18,10 @@ router.post("/history", (req, res) => {
|
||||||
res.send([]);
|
res.send([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/ignore", (req, res) => {
|
||||||
|
if (!req.body || !req.body.id)
|
||||||
|
return res.status(400).send("'id' is required!");
|
||||||
|
ignoreResult(req.body).then(() => res.sendStatus(200));
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
import vitals from "../routes/vitals-route.js";
|
||||||
import results from "../routes/results-route.js";
|
import results from "../routes/results-route.js";
|
||||||
import alerting from "../routes/alerting-route.js";
|
import alerting from "../routes/alerting-route.js";
|
||||||
import react from "../routes/react-route.js";
|
import react from "../routes/react-route.js";
|
||||||
|
@ -12,6 +13,7 @@ import buildDevRoute from "../routes/dev-route.js";
|
||||||
export default function buildRoutes(pg, skio) {
|
export default function buildRoutes(pg, skio) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
// Special Routes
|
// Special Routes
|
||||||
|
router.use(vitals);
|
||||||
router.all("/", (req, res) => res.redirect("/qualiteer"));
|
router.all("/", (req, res) => res.redirect("/qualiteer"));
|
||||||
if (process.env.USE_DEV_ROUTER === "true")
|
if (process.env.USE_DEV_ROUTER === "true")
|
||||||
router.use("/api/dev", buildDevRoute(pg, skio));
|
router.use("/api/dev", buildDevRoute(pg, skio));
|
||||||
|
|
7
lib/routes/vitals-route.js
Normal file
7
lib/routes/vitals-route.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Get Routes
|
||||||
|
router.get("/healthz", (req, res) => res.sendStatus(200));
|
||||||
|
|
||||||
|
export default router;
|
|
@ -13,7 +13,7 @@ const OUT = "o";
|
||||||
|
|
||||||
export default class Executor {
|
export default class Executor {
|
||||||
constructor(config, payload) {
|
constructor(config, payload) {
|
||||||
this.url = config.url(payload) ?? process.env.QUALITEER_URL;
|
this.url = config.url(payload) ?? process.env.QUALITEER_EXECUTOR_URL;
|
||||||
this.jobId = config.jobId(payload) ?? process.env.QUALITEER_JOB_ID;
|
this.jobId = config.jobId(payload) ?? process.env.QUALITEER_JOB_ID;
|
||||||
this.command = config.command(payload) ?? process.env.QUALITEER_COMMAND;
|
this.command = config.command(payload) ?? process.env.QUALITEER_COMMAND;
|
||||||
this.mode = modes.EXEC;
|
this.mode = modes.EXEC;
|
||||||
|
|
5655
package-lock.json
generated
5655
package-lock.json
generated
File diff suppressed because it is too large
Load diff
59
package.json
59
package.json
|
@ -14,18 +14,18 @@
|
||||||
"qualiteer": "./dist/app.js"
|
"qualiteer": "./dist/app.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:all": "npm run build:react && npm run build:executor",
|
"build:all": "concurrently \"npm run build:react\" \"npm run build:executor\" -n v,s -p -c yellow,green",
|
||||||
"build:executor": "node lib/jobs/executor/executor-bundler.js",
|
"build:executor": "node lib/jobs/executor/executor-bundler.js",
|
||||||
"build:react": "vite build",
|
"build:react": "vite build",
|
||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"start:dev": "nodemon dist/app.js",
|
"dev:server": "nodemon dist/app.js",
|
||||||
"start:dev:replit": "npm run start:react:replit & (sleep 30 && npm run start:dev)",
|
"dev:react": "vite",
|
||||||
"start:react": "vite preview",
|
"start:dev": "concurrently -k \"QUALITEER_DEV_PORT=52025 npm run dev:server\" \" QUALITEER_VITE_DEV_PORT=52000 QUALITEER_VITE_BACKEND_URL=http://localhost:52025 npm run dev:react\" -n s,v -p -c green,yellow",
|
||||||
"start:react:replit": "vite --host",
|
|
||||||
"test": "node tests/index.js",
|
"test": "node tests/index.js",
|
||||||
"test:api": "node tests/api.js",
|
"test:api": "node tests/api.js",
|
||||||
"test:dev": "nodemon tests/index.js",
|
"test:dev": "nodemon tests/index.js",
|
||||||
"lint": "prettier -w lib/ src/"
|
"lint": "prettier -w lib/ src/",
|
||||||
|
"seed": "node lib/database/seed.js"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
@ -40,44 +40,45 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"amqplib": "^0.8.0",
|
"@kubernetes/client-node": "^0.17.0",
|
||||||
|
"amqplib": "^0.10.3",
|
||||||
"chalk": "^5.0.1",
|
"chalk": "^5.0.1",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.2",
|
||||||
"express": "^4.17.2",
|
"express": "^4.18.1",
|
||||||
"figlet": "^1.5.2",
|
"figlet": "^1.5.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.4",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pg-promise": "^10.11.1",
|
"pg-promise": "^10.12.0",
|
||||||
"postgres-migrations": "^5.3.0",
|
"postgres-migrations": "^5.3.0",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.5.2",
|
||||||
"socket.io-client": "^4.4.1",
|
"socket.io-client": "^4.5.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.9.0",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@mui/icons-material": "^5.6.2",
|
"@mui/icons-material": "^5.10.3",
|
||||||
"@mui/material": "^5.6.4",
|
"@mui/material": "^5.10.4",
|
||||||
"@rollup/plugin-commonjs": "^22.0.0",
|
"@rollup/plugin-commonjs": "^22.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
"@rollup/plugin-node-resolve": "^14.0.0",
|
||||||
"@rollup/plugin-replace": "^4.0.0",
|
"@rollup/plugin-replace": "^4.0.0",
|
||||||
"@tanstack/react-query": "^4.0.0",
|
"@tanstack/react-query": "^4.2.3",
|
||||||
"@vitejs/plugin-react": "1.1.1",
|
"@vitejs/plugin-react": "2.1.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"caxa": "^2.1.0",
|
"caxa": "^2.1.0",
|
||||||
"nodemon": "^2.0.15",
|
"concurrently": "^7.3.0",
|
||||||
|
"nodemon": "^2.0.19",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react": "^18.1.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"readline-sync": "^1.4.10",
|
"readline-sync": "^1.4.10",
|
||||||
"rollup": "^2.72.0",
|
"rollup": "^2.79.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"vite": "2.7.0"
|
"vite": "3.1.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"rabbiteer": "gitlab:Dunemask/rabbiteer"
|
"rabbiteer": "gitlab:Dunemask/rabbiteer"
|
||||||
},
|
}
|
||||||
"proxy": "http://localhost:52000/"
|
|
||||||
}
|
}
|
||||||
|
|
10
replit.nix
10
replit.nix
|
@ -1,10 +0,0 @@
|
||||||
{ pkgs }: {
|
|
||||||
deps = [
|
|
||||||
pkgs.cloc
|
|
||||||
pkgs.nodejs-16_x
|
|
||||||
pkgs.nodePackages.typescript-language-server
|
|
||||||
pkgs.nodePackages.yarn
|
|
||||||
pkgs.replitPackages.jest
|
|
||||||
pkgs.vim
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ const ACTIONS = {
|
||||||
PIPELINE: "p",
|
PIPELINE: "p",
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = "https://qualiteer.elijahparker3.repl.co/";
|
const url = "/";
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
jobs: [],
|
jobs: [],
|
||||||
|
@ -134,7 +134,7 @@ export const JobProvider = ({ children }) => {
|
||||||
request,
|
request,
|
||||||
onLog,
|
onLog,
|
||||||
onClose,
|
onClose,
|
||||||
null,
|
() => {},
|
||||||
onPipelineTrigger
|
onPipelineTrigger
|
||||||
);
|
);
|
||||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||||
|
@ -146,6 +146,7 @@ export const JobProvider = ({ children }) => {
|
||||||
const pipelineReq = {
|
const pipelineReq = {
|
||||||
image: "node",
|
image: "node",
|
||||||
pipeline: { __test, triggers: { ...tree[__test] } },
|
pipeline: { __test, triggers: { ...tree[__test] } },
|
||||||
|
isTriage: builderCache.triageFailing,
|
||||||
};
|
};
|
||||||
const id = `pij${Date.now()}`;
|
const id = `pij${Date.now()}`;
|
||||||
const pipeline = { id, branches, pendingTriggers: [], selectedBranches };
|
const pipeline = { id, branches, pendingTriggers: [], selectedBranches };
|
||||||
|
@ -232,6 +233,7 @@ export const JobProvider = ({ children }) => {
|
||||||
image: "node",
|
image: "node",
|
||||||
type: "single",
|
type: "single",
|
||||||
name: jobId,
|
name: jobId,
|
||||||
|
isTriage: builderCache.isTriage,
|
||||||
};
|
};
|
||||||
|
|
||||||
jobCreate(job);
|
jobCreate(job);
|
||||||
|
@ -250,7 +252,7 @@ export const JobProvider = ({ children }) => {
|
||||||
jobUpdate({ ...job }, jobId);
|
jobUpdate({ ...job }, jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const started = i.newJob(request, onLog, onClose);
|
const started = i.newJob(request, onLog, onClose, () => {});
|
||||||
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
started.then(() => jobUpdate({ status: jobStatus.ACTIVE }, jobId));
|
||||||
|
|
||||||
return jobId;
|
return jobId;
|
||||||
|
|
|
@ -5,7 +5,7 @@ const ACTIONS = {
|
||||||
UPDATE: "u",
|
UPDATE: "u",
|
||||||
};
|
};
|
||||||
|
|
||||||
const localStorage = { setItem: () => {}, getItem: () => {} };
|
// const localStorage = { setItem: () => {}, getItem: () => {} };
|
||||||
|
|
||||||
const localSettings = localStorage.getItem("settings");
|
const localSettings = localStorage.getItem("settings");
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
|
@ -14,6 +14,7 @@ const defaultSettings = {
|
||||||
logAppDetails: true,
|
logAppDetails: true,
|
||||||
defaultRegion: "us",
|
defaultRegion: "us",
|
||||||
defaultPage: "failing",
|
defaultPage: "failing",
|
||||||
|
triageFailing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
|
const settings = localSettings ? JSON.parse(localSettings) : defaultSettings;
|
||||||
|
@ -58,7 +59,7 @@ export const StoreProvider = ({ children }) => {
|
||||||
name: silenceInfo.name ?? "*",
|
name: silenceInfo.name ?? "*",
|
||||||
class: silenceInfo.class ?? "*",
|
class: silenceInfo.class ?? "*",
|
||||||
method: silenceInfo.method ?? "*",
|
method: silenceInfo.method ?? "*",
|
||||||
silencedUntil: silenceInfo.silencedUntil,
|
expires: silenceInfo.expires,
|
||||||
};
|
};
|
||||||
console.log("Would upsert silence", req);
|
console.log("Would upsert silence", req);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { silencedMock } from "@qltr/mocks/alerting-mock.js";
|
import { silencedMock } from "@qltr/mocks/alerting-mock.js";
|
||||||
import { failingMock } from "@qltr/mocks/results-mock.js";
|
import { failingMock } from "@qltr/mocks/results-mock.js";
|
||||||
import { testsMock, mappingsMock } from "@qltr/mocks/catalog-mock.js";
|
import { testsMock, mappingsMock } from "@qltr/mocks/catalog-mock.js";
|
||||||
|
|
||||||
const QUALITEER_URL = "https://qualiteer.elijahparker3.repl.co/api";
|
const QUALITEER_URL = "/api";
|
||||||
|
|
||||||
const useMock = true;
|
const useMock = false;
|
||||||
|
|
||||||
const asMock = (data) => ({ data });
|
const asMock = (data) => ({ data });
|
||||||
|
|
||||||
|
@ -31,3 +31,44 @@ export const useCurrentlyFailing = () =>
|
||||||
useMock
|
useMock
|
||||||
? asMock(failingMock())
|
? asMock(failingMock())
|
||||||
: useQuery(["failing"], fetchApi("/results/failing"));
|
: useQuery(["failing"], fetchApi("/results/failing"));
|
||||||
|
|
||||||
|
export const useUpsertAlert = () => {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return async function upsertAlert(silenceRequest) {
|
||||||
|
var { id, name, class: className, method, expires } = silenceRequest;
|
||||||
|
const keepExpires = typeof expires === "string";
|
||||||
|
name = name ? name : "*";
|
||||||
|
className = className ? className : "*";
|
||||||
|
method = method ? method : "*";
|
||||||
|
const res = await fetch(`${QUALITEER_URL}/alerting/silence`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
class: className,
|
||||||
|
method,
|
||||||
|
expires,
|
||||||
|
keepExpires,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
qc.invalidateQueries(["silenced"]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIgnoreResult = () => {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return async function ignoreResult(result) {
|
||||||
|
const { id } = result;
|
||||||
|
const res = await fetch(`${QUALITEER_URL}/results/ignore`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id }),
|
||||||
|
});
|
||||||
|
qc.invalidateQueries(["failing"]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function About() {
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Qualiteer was designed to solve the issue of "on call". A state of
|
Qualiteer was designed to solve the issue of "on call". A state of
|
||||||
being in which QA tests will fail, stiring everyone into a frenzy of
|
being in which QA tests will fail, stirring everyone into a frenzy of
|
||||||
what is broken in production! 🤯 Qualiteer gives users power to
|
what is broken in production! 🤯 Qualiteer gives users power to
|
||||||
resolve and reattempt failing tests, run a particular suite of tests,
|
resolve and reattempt failing tests, run a particular suite of tests,
|
||||||
and mute pesky alerts reminding you the navbar's color changed... 🤦♂️
|
and mute pesky alerts reminding you the navbar's color changed... 🤦♂️
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext } from "react";
|
||||||
import { useSilencedAlerts } from "@qltr/queries";
|
import { useSilencedAlerts, useUpsertAlert } from "@qltr/queries";
|
||||||
import StoreContext from "@qltr/store";
|
import StoreContext from "@qltr/store";
|
||||||
import SilencedBox from "./SilencedBox.jsx";
|
import SilencedBox from "./SilencedBox.jsx";
|
||||||
import SilenceDialog from "./SilenceDialog.jsx";
|
import SilenceDialog, { useSilenceDialog } from "./SilenceDialog.jsx";
|
||||||
|
|
||||||
import SpeedDial from "@mui/material/SpeedDial";
|
import SpeedDial from "@mui/material/SpeedDial";
|
||||||
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
import SpeedDialAction from "@mui/material/SpeedDialAction";
|
||||||
|
@ -18,41 +18,38 @@ import Typography from "@mui/material/Typography";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
export default function Alerting() {
|
export default function Alerting() {
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const { updateStore, silenceRequest } = useContext(StoreContext);
|
const { updateStore, silenceRequest } = useContext(StoreContext);
|
||||||
|
|
||||||
const { isLoading, data: silenced } = useSilencedAlerts();
|
const { isLoading, data: silenced } = useSilencedAlerts();
|
||||||
|
const upsertAlert = useUpsertAlert();
|
||||||
|
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||||
|
const toggleDelete = () => setDeleteOpen(!deleteOpen);
|
||||||
|
|
||||||
const [silenceEntry, setSilenceEntry] = useState({
|
const handleDeleteClose = (confirmed) => () => {
|
||||||
open: false,
|
toggleDelete();
|
||||||
deleteOpen: false,
|
if (!confirmed) return;
|
||||||
});
|
const silenceReq = { ...silence };
|
||||||
|
upsertAlert({ ...silenceReq, expires: null });
|
||||||
const closeSilence = () =>
|
|
||||||
setSilenceEntry({ ...silenceEntry, open: false, deleteOpen: false });
|
|
||||||
|
|
||||||
const handleDeleteClose = (makeRequest) => () => {
|
|
||||||
const silenceReq = { ...silenceEntry };
|
|
||||||
closeSilence();
|
|
||||||
if (!makeRequest) return;
|
|
||||||
silenceRequest({ ...silenceReq, silencedUntil: null });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (silenceReq) => {
|
const handleClose = (silenceReq) => {
|
||||||
closeSilence();
|
|
||||||
if (!silenceReq) return;
|
if (!silenceReq) return;
|
||||||
silenceRequest(silenceReq);
|
upsertAlert(silenceReq);
|
||||||
};
|
};
|
||||||
|
|
||||||
const quickAlertClick = () => {
|
const quickAlertClick = () => {
|
||||||
setSilenceEntry({ open: true, deleteOpen: false });
|
setSilence({});
|
||||||
|
sdToggle();
|
||||||
};
|
};
|
||||||
|
|
||||||
const editSilence = (silence) => () => {
|
const editSilence = (silence) => () => {
|
||||||
setSilenceEntry({ ...silence, open: true, deleteOpen: false });
|
sdToggle();
|
||||||
|
setSilence(silence);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSilence = (silence) => () => {
|
const removeSilence = (silence) => () => {
|
||||||
setSilenceEntry({ ...silence, deleteOpen: true, open: false });
|
setSilence(silence);
|
||||||
|
toggleDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,7 +90,7 @@ export default function Alerting() {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={silenceEntry.deleteOpen}
|
open={deleteOpen}
|
||||||
onClose={handleDeleteClose()}
|
onClose={handleDeleteClose()}
|
||||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
|
@ -110,9 +107,9 @@ export default function Alerting() {
|
||||||
|
|
||||||
<SilenceDialog
|
<SilenceDialog
|
||||||
keepMounted
|
keepMounted
|
||||||
open={silenceEntry.open}
|
open={sdOpen}
|
||||||
onClose={handleClose}
|
onClose={sdClose}
|
||||||
silence={silenceEntry}
|
silence={silence}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SpeedDial
|
<SpeedDial
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useState, useContext, useEffect } from "react";
|
import { useState, useContext, useEffect } from "react";
|
||||||
import StoreContext from "@qltr/store";
|
import { useUpsertAlert } from "@qltr/queries";
|
||||||
|
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
@ -11,25 +10,53 @@ import Dialog from "@mui/material/Dialog";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
|
|
||||||
|
import SilenceSelector from "./SilenceSelector.jsx";
|
||||||
|
export function useSilenceDialog(isOpen = false) {
|
||||||
|
const [open, setOpen] = useState(isOpen);
|
||||||
|
const [silence, setSilence] = useState({});
|
||||||
|
const upsertAlert = useUpsertAlert();
|
||||||
|
const dialogToggle = () => setOpen(!open);
|
||||||
|
const dialogClose = (confirmedSilence) => {
|
||||||
|
setOpen(false);
|
||||||
|
if (!confirmedSilence) return;
|
||||||
|
upsertAlert(confirmedSilence);
|
||||||
|
setSilence({});
|
||||||
|
};
|
||||||
|
return [open, dialogToggle, silence, setSilence, dialogClose];
|
||||||
|
}
|
||||||
|
|
||||||
export default function SilenceDialog(props) {
|
export default function SilenceDialog(props) {
|
||||||
const { silence, open, onClose } = props;
|
const { silence, open, onClose } = props;
|
||||||
|
const [duration, setDuration] = useState({ h: 0, m: 0 });
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
const [silenceEntry, setSilenceEntry] = useState(silence);
|
const [silenceEntry, setSilenceEntry] = useState(silence);
|
||||||
|
|
||||||
|
const durationModified = duration.h !== 0 || duration.m !== 0;
|
||||||
|
const silenceModified =
|
||||||
|
silence.name !== silenceEntry.name ||
|
||||||
|
silence.method !== silenceEntry.method ||
|
||||||
|
silence.class !== silenceEntry.class;
|
||||||
|
const modified =
|
||||||
|
(Object.keys(silence).length > 0 && durationModified) || silenceModified;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSilenceEntry(silence);
|
setSilenceEntry(silence);
|
||||||
|
setDuration({ h: 0, m: 0 });
|
||||||
}, [silence, open]);
|
}, [silence, open]);
|
||||||
|
|
||||||
const { state: store, updateStore } = useContext(StoreContext);
|
|
||||||
|
|
||||||
const handleCancel = () => onClose();
|
const handleCancel = () => onClose();
|
||||||
|
|
||||||
const handleOk = () => onClose(silenceEntry);
|
const handleOk = () => onClose(silenceEntry);
|
||||||
|
|
||||||
const updateSilence = (silenceType) => (e) => {
|
const updateSilence = (silenceType) => (e) => {
|
||||||
silenceEntry[silenceType] = e.target.value;
|
const s = { ...silenceEntry };
|
||||||
setSilenceEntry({ ...silenceEntry });
|
s[silenceType] = e.target.value;
|
||||||
|
setSilenceEntry({ ...s, expires: duration });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDuration = (dur) => {
|
||||||
|
setDuration(dur);
|
||||||
|
setSilenceEntry({ ...silenceEntry, expires: dur });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +64,7 @@ export default function SilenceDialog(props) {
|
||||||
sx={
|
sx={
|
||||||
fullScreen
|
fullScreen
|
||||||
? {}
|
? {}
|
||||||
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }
|
: { "& .MuiDialog-paper": { width: "80%", maxHeight: 525 } }
|
||||||
}
|
}
|
||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
open={open}
|
open={open}
|
||||||
|
@ -70,12 +97,19 @@ export default function SilenceDialog(props) {
|
||||||
onChange={updateSilence("class")}
|
onChange={updateSilence("class")}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
|
<SilenceSelector
|
||||||
|
formerExpires={silence.expires}
|
||||||
|
duration={duration}
|
||||||
|
setDuration={updateDuration}
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button autoFocus onClick={handleCancel}>
|
<Button autoFocus onClick={handleCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOk}>Ok</Button>
|
<Button onClick={handleOk} disabled={!modified}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
70
src/views/alerting/SilenceSelector.jsx
Normal file
70
src/views/alerting/SilenceSelector.jsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
|
||||||
|
const MAX_DURATION = { h: 72, m: 59 };
|
||||||
|
const MIN_DURATION = 0;
|
||||||
|
export default function SilenceSelector(props) {
|
||||||
|
const [expires, setExpires] = useState(moment());
|
||||||
|
const { formerExpires, duration, setDuration } = props;
|
||||||
|
const formerExpirey =
|
||||||
|
formerExpires && duration.h === 0 && duration.m === 0
|
||||||
|
? moment(formerExpires)
|
||||||
|
: null;
|
||||||
|
const updateDuration = (t) => (e) => {
|
||||||
|
const d = { ...duration };
|
||||||
|
d[t] = Math.abs(parseInt(e.target.value));
|
||||||
|
if (isNaN(d[t])) d[t] = 0;
|
||||||
|
d[t] = d[t] <= MAX_DURATION[t] ? d[t] : MAX_DURATION[t];
|
||||||
|
setDuration(d);
|
||||||
|
const now = moment().add(d.h, "hours").add(d.m, "minutes");
|
||||||
|
setExpires(now);
|
||||||
|
};
|
||||||
|
const onNumberInput = (e) =>
|
||||||
|
(e.target.value =
|
||||||
|
!!e.target.value && Math.abs(e.target.value) >= MIN_DURATION
|
||||||
|
? Math.abs(e.target.value)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box style={{ display: "flex" }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
label="Hours"
|
||||||
|
type="number"
|
||||||
|
onChange={updateDuration("h")}
|
||||||
|
inputProps={{
|
||||||
|
min: MIN_DURATION,
|
||||||
|
max: MAX_DURATION.h,
|
||||||
|
onInput: onNumberInput,
|
||||||
|
}}
|
||||||
|
value={duration.h}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
sx={{ marginLeft: 1 }}
|
||||||
|
label="Minutes"
|
||||||
|
type="number"
|
||||||
|
onChange={updateDuration("m")}
|
||||||
|
inputProps={{
|
||||||
|
min: MIN_DURATION,
|
||||||
|
max: MAX_DURATION.m,
|
||||||
|
onInput: onNumberInput,
|
||||||
|
}}
|
||||||
|
value={duration.m}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
label="Expires"
|
||||||
|
value={`${(formerExpirey ?? expires).format("L")} ${(
|
||||||
|
formerExpirey ?? expires
|
||||||
|
).format("LT")}`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext } from "react";
|
||||||
|
import moment from "moment";
|
||||||
import StoreContext from "@qltr/store";
|
import StoreContext from "@qltr/store";
|
||||||
|
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
|
@ -18,7 +19,7 @@ export default function SilencingBox(props) {
|
||||||
method: testMethod,
|
method: testMethod,
|
||||||
class: testClass,
|
class: testClass,
|
||||||
id: silenceId,
|
id: silenceId,
|
||||||
silencedUntil,
|
expires,
|
||||||
} = silenceEntry;
|
} = silenceEntry;
|
||||||
|
|
||||||
const { state: store, updateStore } = useContext(StoreContext);
|
const { state: store, updateStore } = useContext(StoreContext);
|
||||||
|
@ -61,11 +62,19 @@ export default function SilencingBox(props) {
|
||||||
<br />
|
<br />
|
||||||
{`Test Class: ${testClass}`}
|
{`Test Class: ${testClass}`}
|
||||||
<br />
|
<br />
|
||||||
{`Silenced Until: ${silencedUntil} Remaining Time: 2:50`}
|
{`Silenced Until: ${expires} Remaining Time: ${moment(
|
||||||
|
moment(expires).diff(moment())
|
||||||
|
).format("HH:mm")}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
sx={{
|
||||||
|
ml: "auto",
|
||||||
|
mb: "auto",
|
||||||
|
mt: "auto",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
display: { md: "none", lg: "none", xl: "none" },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Actions />
|
<Actions />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import TextField from "@mui/material/TextField";
|
||||||
export default function Catalog() {
|
export default function Catalog() {
|
||||||
const { state: store, updateStore } = useContext(StoreContext);
|
const { state: store, updateStore } = useContext(StoreContext);
|
||||||
const { state: jobState } = useContext(JobContext);
|
const { state: jobState } = useContext(JobContext);
|
||||||
const { isLoading, data: tests } = useCatalogTests();
|
const { isLoading, data: rawTests } = useCatalogTests();
|
||||||
const handleSearchChange = (e) =>
|
const handleSearchChange = (e) =>
|
||||||
updateStore({ catalogSearch: e.target.value });
|
updateStore({ catalogSearch: e.target.value });
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ export default function Catalog() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const catalogWithJobs = () => {
|
const catalogWithJobs = () => {
|
||||||
|
const tests = rawTests.map((t) => ({ ...t, isPipeline: t.pipeline }));
|
||||||
|
for (var t of tests) delete t.pipeline;
|
||||||
for (var test of tests) {
|
for (var test of tests) {
|
||||||
if (test.isPipeline) {
|
if (test.isPipeline) {
|
||||||
const pipeline = jobState.pipelines.find((p) =>
|
const pipeline = jobState.pipelines.find((p) =>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
useJobNav,
|
useJobNav,
|
||||||
} from "@qltr/util/JobTools";
|
} from "@qltr/util/JobTools";
|
||||||
|
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
|
@ -40,8 +42,9 @@ export default function CatalogBox(props) {
|
||||||
const { jobFactory } = useContext(JobContext);
|
const { jobFactory } = useContext(JobContext);
|
||||||
const jobNav = useJobNav();
|
const jobNav = useJobNav();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const toggleOpen = () => setOpen(!open);
|
const toggleOpen = () => setOpen(!open);
|
||||||
|
const theme = useTheme();
|
||||||
|
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
const navigateToJob = () => {
|
const navigateToJob = () => {
|
||||||
if (pipeline) return jobNav.toPipeline(pipeline.id);
|
if (pipeline) return jobNav.toPipeline(pipeline.id);
|
||||||
|
@ -50,7 +53,10 @@ export default function CatalogBox(props) {
|
||||||
|
|
||||||
const runTest = () => {
|
const runTest = () => {
|
||||||
if (isPipeline) return runPipelineTest();
|
if (isPipeline) return runPipelineTest();
|
||||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
const jobId = jobFactory({
|
||||||
|
testNames: [testName],
|
||||||
|
isTriage: store.triageFailing,
|
||||||
|
});
|
||||||
if (store.focusJob) jobNav.toJob(jobId);
|
if (store.focusJob) jobNav.toJob(jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,21 +92,6 @@ export default function CatalogBox(props) {
|
||||||
return useJobIconState(job);
|
return useJobIconState(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Actions() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<IconButton
|
|
||||||
color="success"
|
|
||||||
aria-label="play"
|
|
||||||
component="span"
|
|
||||||
onClick={jobOnClick}
|
|
||||||
>
|
|
||||||
{jobIcon()}
|
|
||||||
</IconButton>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
expanded={open}
|
expanded={open}
|
||||||
|
@ -129,20 +120,18 @@ export default function CatalogBox(props) {
|
||||||
</Box>
|
</Box>
|
||||||
<br />
|
<br />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
direction={minifyActions ? "column" : "row"}
|
||||||
|
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||||
>
|
>
|
||||||
<Actions />
|
<IconButton
|
||||||
</Stack>
|
color="success"
|
||||||
<Stack
|
aria-label="play"
|
||||||
direction="row"
|
component="span"
|
||||||
sx={{
|
onClick={jobOnClick}
|
||||||
ml: "auto",
|
>
|
||||||
display: { xs: "none", sm: "none", md: "flex", lg: "flex" },
|
{jobIcon()}
|
||||||
}}
|
</IconButton>
|
||||||
>
|
|
||||||
<Actions />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
|
|
|
@ -1,135 +1,103 @@
|
||||||
import { useState, useContext } from "react";
|
import { useState, useContext } from "react";
|
||||||
import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries";
|
import { useCurrentlyFailing, useSilencedAlerts } from "@qltr/queries";
|
||||||
import StoreContext from "@qltr/store";
|
|
||||||
import JobContext from "@qltr/jobs";
|
import JobContext from "@qltr/jobs";
|
||||||
import { useJobNav } from "@qltr/util/JobTools";
|
import { useJobNav } from "@qltr/util/JobTools";
|
||||||
import SilenceDialog from "../alerting/SilenceDialog.jsx";
|
import SilenceDialog, { useSilenceDialog } from "../alerting/SilenceDialog.jsx";
|
||||||
import FailingBox from "./FailingBox.jsx";
|
import FailingBox from "./FailingBox.jsx";
|
||||||
|
import QuickSilence, { useQuickSilence } from "./QuickSilence.jsx";
|
||||||
|
import FailingRetry from "./FailingRetry.jsx";
|
||||||
|
|
||||||
import SpeedDial from "@mui/material/SpeedDial";
|
// MaterialUI
|
||||||
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import ReplayIcon from "@mui/icons-material/Replay";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
export default function Failing() {
|
export default function Failing() {
|
||||||
const { state: jobState, retryAll } = useContext(JobContext);
|
const { state: jobState } = useContext(JobContext);
|
||||||
const { state: store, silenceRequest } = useContext(StoreContext);
|
const { isLoading: failsLoading, data: failing } = useCurrentlyFailing();
|
||||||
const { isLoading, data: failing } = useCurrentlyFailing();
|
const { isLoading: silencesLoading, data: silences } = useSilencedAlerts();
|
||||||
const { isSilencedLoading, data: silencedAlerts } = useSilencedAlerts();
|
const [qsAnchor, qsTest, openQs, closeQs] = useQuickSilence();
|
||||||
const jobNav = useJobNav();
|
const [sdOpen, sdToggle, silence, setSilence, sdClose] = useSilenceDialog();
|
||||||
const [silenceEntry, setSilenceEntry] = useState({ open: false });
|
|
||||||
|
|
||||||
const closeSilence = () => setSilenceEntry({ ...silenceEntry, open: false });
|
const joinSilence = (result) => {
|
||||||
|
if (!silences) return;
|
||||||
const handleSilenceClose = (silenceReq) => {
|
const silence = silences.find((s) => s.name === result.name);
|
||||||
closeSilence();
|
if (silence) result.silence = silence;
|
||||||
if (!silenceReq) return;
|
|
||||||
silenceRequest(silenceReq);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const editSilence = (silence) => () => {
|
const joinJob = (result) => {
|
||||||
setSilenceEntry({ ...silence, open: true });
|
if (result.isPipeline) return;
|
||||||
|
const job = jobState.jobs.find(
|
||||||
|
(j) => !j.isPipeline && j.builderCache.testNames.includes(result.name)
|
||||||
|
);
|
||||||
|
if (job) result.job = job;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [retryAllOpen, setRetryAllOpen] = useState(false);
|
const joinPipeline = (result) => {
|
||||||
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
|
if (!result.isPipeline) return;
|
||||||
|
const pipeline = jobState.pipelines.find((p) =>
|
||||||
const handleClose = (confirmed) => () => {
|
p.selectedBranches.find((b) => b.name === result.name)
|
||||||
retryAllClick();
|
);
|
||||||
if (!confirmed) return;
|
if (!pipeline) return;
|
||||||
const jobId = retryAll(store.failing);
|
const pipelineJob = jobState.jobs.find(
|
||||||
|
(j) =>
|
||||||
if (!store.focusJob) return;
|
j.isPipeline &&
|
||||||
jobNav.toJob(jobId);
|
j.pipelineId === pipeline.id &&
|
||||||
|
j.branchId === result.name
|
||||||
|
);
|
||||||
|
if (!pipelineJob) result.pipeline = pipeline;
|
||||||
|
else result.job = pipelineJob;
|
||||||
};
|
};
|
||||||
|
|
||||||
const failingTestsWithJobs = () => {
|
const resultsCompiled = () => {
|
||||||
if (isLoading) return [];
|
if (failsLoading) return [];
|
||||||
const silences = silencedAlerts ?? [];
|
const compiled = [];
|
||||||
for (var test of failing) {
|
for (var r of failing) {
|
||||||
const silence = silences.find(
|
const rc = { ...r };
|
||||||
(s) => s.name === test.name || s.class === test.class
|
joinSilence(rc);
|
||||||
);
|
// Find associated job and skip pipeline
|
||||||
|
joinJob(rc);
|
||||||
if (silence) test.silencedUntil = silence;
|
// Find associated pipeline
|
||||||
if (test.isPipeline) {
|
joinPipeline(rc);
|
||||||
const pipeline = jobState.pipelines.find((p) =>
|
compiled.push(rc);
|
||||||
p.selectedBranches.find((b) => b.name === 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 compiled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const alertClick = (result) => (e) => {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
if (e && e.type === "contextmenu" && result.silence)
|
||||||
|
return openQs(e, result);
|
||||||
|
const { name, class: className, method } = result;
|
||||||
|
const testInfo = { name, class: className, method };
|
||||||
|
const silenceInfo = result.silence ?? testInfo;
|
||||||
|
sdToggle();
|
||||||
|
setSilence(silenceInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="failing">
|
<div className="failing">
|
||||||
{isLoading
|
|
||||||
? null
|
|
||||||
: failingTestsWithJobs().map((v, i) => (
|
|
||||||
<FailingBox key={i} failingTest={v} silenceClick={editSilence(v)} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={retryAllOpen}
|
|
||||||
onClose={handleClose()}
|
|
||||||
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
|
||||||
maxWidth="xs"
|
|
||||||
>
|
|
||||||
<DialogTitle>Retry all failing tests?</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
This will create x jobs and run y tests
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleClose()}>Cancel</Button>
|
|
||||||
<Button onClick={handleClose(true)} autoFocus>
|
|
||||||
Yes
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
<SilenceDialog
|
<SilenceDialog
|
||||||
keepMounted
|
keepMounted
|
||||||
open={silenceEntry.open}
|
open={sdOpen}
|
||||||
onClose={handleSilenceClose}
|
onClose={sdClose}
|
||||||
silence={silenceEntry}
|
silence={silence}
|
||||||
/>
|
/>
|
||||||
{(failing ?? []).length === 0 ? (
|
<QuickSilence
|
||||||
|
anchorEl={qsAnchor}
|
||||||
|
handleClose={closeQs}
|
||||||
|
test={qsTest}
|
||||||
|
editSilence={alertClick(qsTest)}
|
||||||
|
/>
|
||||||
|
<FailingRetry failing={failsLoading ? [] : failing} />
|
||||||
|
{!failsLoading && failing.length === 0 && (
|
||||||
<Box display="flex" alignItems="center" justifyContent="center">
|
<Box display="flex" alignItems="center" justifyContent="center">
|
||||||
<Typography variant="h4">No tests failing!</Typography>
|
<Typography variant="h4">No tests failing!</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{(failing ?? []).length === 0 ? null : (
|
|
||||||
<SpeedDial
|
|
||||||
ariaLabel="Retry All"
|
|
||||||
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
|
||||||
icon={<ReplayIcon />}
|
|
||||||
onClick={retryAllClick}
|
|
||||||
open={false}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
{!failsLoading &&
|
||||||
|
resultsCompiled().map((v, i) => (
|
||||||
|
<FailingBox key={i} failingTest={v} alertClick={alertClick(v)} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext } from "react";
|
||||||
import { usePipelineMappings } from "@qltr/queries";
|
import { usePipelineMappings, useIgnoreResult } from "@qltr/queries";
|
||||||
import StoreContext from "@qltr/store";
|
import StoreContext from "@qltr/store";
|
||||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||||
import {
|
import {
|
||||||
|
@ -7,7 +7,8 @@ import {
|
||||||
usePipelineIconState,
|
usePipelineIconState,
|
||||||
useJobNav,
|
useJobNav,
|
||||||
} from "@qltr/util/JobTools";
|
} from "@qltr/util/JobTools";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
|
@ -42,30 +43,33 @@ import { asTree, asBranches, as1d } from "@qltr/util/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) {
|
||||||
const { failingTest, silenceClick } = props;
|
const { failingTest, alertClick } = props;
|
||||||
const {
|
const {
|
||||||
class: testClass,
|
class: testClass,
|
||||||
name: testName,
|
name: testName,
|
||||||
timestamp,
|
timestamp,
|
||||||
silencedUntil,
|
silence,
|
||||||
type,
|
type,
|
||||||
dailyFails,
|
dailyFails,
|
||||||
screenshot: screenshotUrl,
|
screenshot: screenshotUrl,
|
||||||
|
console: cs,
|
||||||
recentResults,
|
recentResults,
|
||||||
failedMessage,
|
message,
|
||||||
isPipeline,
|
isPipeline,
|
||||||
job,
|
job,
|
||||||
pipeline,
|
pipeline,
|
||||||
} = failingTest;
|
} = failingTest;
|
||||||
|
const ignoreResult = useIgnoreResult();
|
||||||
|
const runHistory = recentResults ? [...recentResults].reverse() : null;
|
||||||
|
|
||||||
const { data: pipelineMappings, isLoading } = usePipelineMappings();
|
const { data: pipelineMappings, isLoading } = usePipelineMappings();
|
||||||
const { jobFactory } = useContext(JobContext);
|
const { jobFactory } = useContext(JobContext);
|
||||||
const { state: store, updateStore, removeFailure } = useContext(StoreContext);
|
const { state: store, updateStore, removeFailure } = useContext(StoreContext);
|
||||||
const jobNav = useJobNav();
|
const jobNav = useJobNav();
|
||||||
|
const theme = useTheme();
|
||||||
|
const minifyActions = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const toggleOpen = () => setOpen(!open);
|
const toggleOpen = () => setOpen(!open);
|
||||||
|
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const removeClick = () => setRemoveOpen(!removeOpen);
|
const removeClick = () => setRemoveOpen(!removeOpen);
|
||||||
|
|
||||||
|
@ -73,7 +77,7 @@ export default function FailingBox(props) {
|
||||||
stopPropagation(e);
|
stopPropagation(e);
|
||||||
setRemoveOpen(false);
|
setRemoveOpen(false);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
removeFailure(failingTest);
|
ignoreResult(failingTest);
|
||||||
};
|
};
|
||||||
|
|
||||||
function badgeColor() {
|
function badgeColor() {
|
||||||
|
@ -91,7 +95,7 @@ export default function FailingBox(props) {
|
||||||
branches: asBranches(primaries),
|
branches: asBranches(primaries),
|
||||||
tree: asTree(primaries),
|
tree: asTree(primaries),
|
||||||
selectedBranches: as1d(primaries),
|
selectedBranches: as1d(primaries),
|
||||||
isTriage: true,
|
isTriage: store.triageFailing,
|
||||||
};
|
};
|
||||||
const pipeline = jobFactory(builderCache);
|
const pipeline = jobFactory(builderCache);
|
||||||
if (store.focusJob) jobNav.toPipeline(pipeline.id);
|
if (store.focusJob) jobNav.toPipeline(pipeline.id);
|
||||||
|
@ -99,7 +103,10 @@ export default function FailingBox(props) {
|
||||||
|
|
||||||
const retryTest = () => {
|
const retryTest = () => {
|
||||||
if (isPipeline) return retryPipelineTest();
|
if (isPipeline) return retryPipelineTest();
|
||||||
const jobId = jobFactory({ testNames: [testName], isTriage: true });
|
const jobId = jobFactory({
|
||||||
|
testNames: [testName],
|
||||||
|
isTriage: store.triageFailing,
|
||||||
|
});
|
||||||
if (store.focusJob) jobNav.toJob(jobId);
|
if (store.focusJob) jobNav.toJob(jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,39 +127,6 @@ export default function FailingBox(props) {
|
||||||
return useJobIconState(job);
|
return useJobIconState(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Actions() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<a href={screenshotUrl}>
|
|
||||||
<IconButton aria-label="photo" component="span">
|
|
||||||
<PhotoCameraIcon />
|
|
||||||
</IconButton>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
|
||||||
{jobIcon()}
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
aria-label="silence"
|
|
||||||
component="span"
|
|
||||||
color={silencedUntil ? "primary" : "default"}
|
|
||||||
onClick={silenceClick}
|
|
||||||
>
|
|
||||||
<NotificationsIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
aria-label="delete"
|
|
||||||
component="span"
|
|
||||||
onClick={removeClick}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
expanded={open}
|
expanded={open}
|
||||||
|
@ -212,12 +186,23 @@ export default function FailingBox(props) {
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<span className="recent-results">
|
<span className="recent-results">
|
||||||
{recentResults.map(
|
{runHistory &&
|
||||||
(v, i) =>
|
runHistory.map(
|
||||||
(v && <CheckIcon key={i} color="success" />) || (
|
(v, i) =>
|
||||||
<ClearIcon key={i} color="error" />
|
(!v.failed && (
|
||||||
)
|
<CheckIcon
|
||||||
)}
|
key={i}
|
||||||
|
color="success"
|
||||||
|
titleAccess={v.timestamp}
|
||||||
|
/>
|
||||||
|
)) || (
|
||||||
|
<ClearIcon
|
||||||
|
key={i}
|
||||||
|
color="error"
|
||||||
|
titleAccess={v.timestamp}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{isPipeline && <ViewColumnIcon />}
|
{isPipeline && <ViewColumnIcon />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -225,27 +210,41 @@ export default function FailingBox(props) {
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
onClick={stopPropagation}
|
onClick={stopPropagation}
|
||||||
sx={{ ml: "auto", display: { md: "none", lg: "none", xl: "none" } }}
|
direction={minifyActions ? "column" : "row"}
|
||||||
|
sx={{ ml: "auto", mb: "auto", mt: "auto", whiteSpace: "nowrap" }}
|
||||||
>
|
>
|
||||||
<Actions />
|
<a href={screenshotUrl}>
|
||||||
</Stack>
|
<IconButton aria-label="photo" component="span">
|
||||||
<Stack
|
<PhotoCameraIcon />
|
||||||
onClick={stopPropagation}
|
</IconButton>
|
||||||
direction="row"
|
</a>
|
||||||
sx={{
|
|
||||||
ml: "auto",
|
<IconButton aria-label="retry" component="span" onClick={jobOnClick}>
|
||||||
mb: "auto",
|
{jobIcon()}
|
||||||
mt: "auto",
|
</IconButton>
|
||||||
whiteSpace: "nowrap",
|
|
||||||
display: { xs: "none", sm: "none", md: "block", lg: "block" },
|
<IconButton
|
||||||
}}
|
aria-label="silence"
|
||||||
>
|
component="span"
|
||||||
<Actions />
|
color={silence && silence.expires ? "primary" : "default"}
|
||||||
|
onClick={alertClick}
|
||||||
|
onContextMenu={alertClick}
|
||||||
|
>
|
||||||
|
<NotificationsIcon titleAccess={silence && silence.expires} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
aria-label="delete"
|
||||||
|
component="span"
|
||||||
|
onClick={removeClick}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
|
<Typography component={"span"} style={{ wordBreak: "break-word" }}>
|
||||||
{failedMessage}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
64
src/views/failing/FailingRetry.jsx
Normal file
64
src/views/failing/FailingRetry.jsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// React
|
||||||
|
import React, { useState, useContext } from "react";
|
||||||
|
import JobContext from "@qltr/jobs";
|
||||||
|
import StoreContext from "@qltr/store";
|
||||||
|
import { useJobNav } from "@qltr/util/JobTools";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import SpeedDial from "@mui/material/SpeedDial";
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import ReplayIcon from "@mui/icons-material/Replay";
|
||||||
|
|
||||||
|
export default function FailingRetry(props) {
|
||||||
|
const { failing } = props;
|
||||||
|
const { state: jobState, retryAll } = useContext(JobContext);
|
||||||
|
const { state: store } = useContext(StoreContext);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const jobNav = useJobNav();
|
||||||
|
const toggleOpen = () => setOpen(!open);
|
||||||
|
const dialogClose = (confirmed) => () => {
|
||||||
|
toggleOpen();
|
||||||
|
if (!confirmed) return;
|
||||||
|
const jobId = retryAll(failing);
|
||||||
|
if (!store.focusJob) return;
|
||||||
|
jobNav.toJob(jobId);
|
||||||
|
};
|
||||||
|
if (!failing || failing.length === 0) return;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={dialogClose()}
|
||||||
|
sx={{ "& .MuiDialog-paper": { width: "80%", maxHeight: 435 } }}
|
||||||
|
maxWidth="xs"
|
||||||
|
>
|
||||||
|
<DialogTitle>Retry all failing tests?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
This will create x jobs and run y tests
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={dialogClose()}>Cancel</Button>
|
||||||
|
<Button onClick={dialogClose(true)} autoFocus>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<SpeedDial
|
||||||
|
ariaLabel="Retry All"
|
||||||
|
sx={{ position: "fixed", bottom: 16, right: 16 }}
|
||||||
|
icon={<ReplayIcon />}
|
||||||
|
onClick={toggleOpen}
|
||||||
|
open={false}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
58
src/views/failing/QuickSilence.jsx
Normal file
58
src/views/failing/QuickSilence.jsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// React
|
||||||
|
import { useState, useContext } from "react";
|
||||||
|
import { useUpsertAlert } from "@qltr/queries";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import Popover from "@mui/material/Popover";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
|
||||||
|
//Icons
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
|
||||||
|
export function useQuickSilence() {
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const [test, setTest] = useState(null);
|
||||||
|
const openMenuAt = (e, t) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setTest(t);
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
};
|
||||||
|
const closeMenu = () => setAnchorEl(null);
|
||||||
|
return [anchorEl, test, openMenuAt, closeMenu];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuickSilence(props) {
|
||||||
|
const { anchorEl, test, handleClose, editSilence } = props;
|
||||||
|
const upsertAlert = useUpsertAlert();
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
async function deleteClick() {
|
||||||
|
await upsertAlert({ id: test.silence.id, expires: null });
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editClick() {
|
||||||
|
editSilence();
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton color="primary" onClick={editClick}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton color="error" onClick={deleteClick}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||||
import {
|
import {
|
||||||
selectedPipelineBranches,
|
selectedPipelineBranches,
|
||||||
|
@ -37,6 +38,7 @@ function JobPipelineDisplay(props) {
|
||||||
} = useContext(JobContext);
|
} = useContext(JobContext);
|
||||||
|
|
||||||
const jobNav = useJobNav();
|
const jobNav = useJobNav();
|
||||||
|
const nav = useNavigate();
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
@ -92,7 +94,7 @@ function JobPipelineDisplay(props) {
|
||||||
<Toolbar disableGutters />
|
<Toolbar disableGutters />
|
||||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
<IconButton onClick={jobNav.toJobs}>
|
<IconButton onClick={() => nav(-1)}>
|
||||||
<ArrowBackIcon />
|
<ArrowBackIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}>
|
<Typography variant="h6" sx={{ ml: "auto", mr: "auto" }}>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useContext, useState, useEffect } from "react";
|
import React, { useContext, useState, useEffect } from "react";
|
||||||
import { useJobNav } from "@qltr/util/JobTools";
|
import { useJobNav } from "@qltr/util/JobTools";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import JobContext, { jobStatus } from "@qltr/jobs";
|
import JobContext, { jobStatus } from "@qltr/jobs";
|
||||||
import StoreContext from "@qltr/store";
|
import StoreContext from "@qltr/store";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
|
@ -27,6 +28,7 @@ export default function JobView(props) {
|
||||||
const { jobFactory, jobCancel, jobDestroy } = useContext(JobContext);
|
const { jobFactory, jobCancel, jobDestroy } = useContext(JobContext);
|
||||||
const { state: store } = useContext(StoreContext);
|
const { state: store } = useContext(StoreContext);
|
||||||
const jobNav = useJobNav();
|
const jobNav = useJobNav();
|
||||||
|
const nav = useNavigate();
|
||||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
|
@ -91,7 +93,7 @@ export default function JobView(props) {
|
||||||
<Toolbar disableGutters />
|
<Toolbar disableGutters />
|
||||||
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
<Box sx={{ flexGrow: 1, margin: "0 10px" }}>
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
<IconButton onClick={navigateToJobs}>
|
<IconButton onClick={() => nav(-1)}>
|
||||||
<ArrowBackIcon />
|
<ArrowBackIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" component="span" sx={{ ml: "auto" }}>
|
<Typography variant="h6" component="span" sx={{ ml: "auto" }}>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function JobBuilder() {
|
||||||
const handleClose = (confirmed) => () => {
|
const handleClose = (confirmed) => () => {
|
||||||
setJobDialogOpen(false);
|
setJobDialogOpen(false);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
const jobId = jobFactory(cache);
|
const jobId = jobFactory({ ...cache, isTriage: store.triageFailing });
|
||||||
if (store.focusJob) jobNav.toJob(jobId);
|
if (store.focusJob) jobNav.toJob(jobId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,11 @@ export default function Settings(props) {
|
||||||
<Switch edge="end" checked={store.focusJob} />
|
<Switch edge="end" checked={store.focusJob} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem button divider onClick={handleToggle("triageFailing")}>
|
||||||
|
<ListItemText primary="Triage Failing" />
|
||||||
|
<Switch edge="end" checked={store.triageFailing} />
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<ListItem button divider onClick={handleToggle("logAppDetails")}>
|
<ListItem button divider onClick={handleToggle("logAppDetails")}>
|
||||||
<ListItemText primary="Log App Details" />
|
<ListItemText primary="Log App Details" />
|
||||||
<Switch edge="end" checked={store.logAppDetails} />
|
<Switch edge="end" checked={store.logAppDetails} />
|
||||||
|
|
12
tests/api.js
12
tests/api.js
|
@ -7,16 +7,18 @@ import axios from "axios";
|
||||||
const qltr = new Qualiteer();
|
const qltr = new Qualiteer();
|
||||||
await qltr.start();
|
await qltr.start();
|
||||||
|
|
||||||
const url = "https://Qualiteer.elijahparker3.repl.co";
|
const url = process.env.QUALITEER_EXECUTOR_URL;
|
||||||
|
|
||||||
const testsUrl = "/api/catalog/tests";
|
const mappingsUrl = "/api/catalog/pipeline-mappings";
|
||||||
const resultsUrl = "/api/results/failing";
|
const resultsUrl = "/api/results/failing";
|
||||||
|
|
||||||
const get = (...args) => axios.get(`${url}/${args[0]}`, args[1]);
|
const get = (...args) => axios.get(`${url}${args[0]}`, args[1]);
|
||||||
|
|
||||||
var res = await get(resultsUrl);
|
const mappings = await get(resultsUrl, {}).catch((e) => console.log(e));
|
||||||
|
console.log(mappings.data);
|
||||||
|
/*var res = await get(resultsUrl);
|
||||||
console.log(res.data);
|
console.log(res.data);
|
||||||
|
|
||||||
res = await get(resultsUrl, { headers: { count: true } });
|
res = await get(resultsUrl, { headers: { count: true } });
|
||||||
|
|
||||||
console.log(res.data);
|
console.log(res.data);*/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function failingTest() {
|
export default function failingTest() {
|
||||||
console.log("This came from a failing test!");
|
console.log("This came from a failing test!");
|
||||||
return { status: 1 };
|
return { status: 1, message: "This test always fails :(" };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import Rabbiteer from "rabbiteer";
|
||||||
import primary from "./primary.js";
|
import primary from "./primary.js";
|
||||||
import secondary1 from "./secondary1.js";
|
import secondary1 from "./secondary1.js";
|
||||||
import secondary2 from "./secondary2.js";
|
import secondary2 from "./secondary2.js";
|
||||||
|
@ -13,11 +14,15 @@ import failing from "./failing.js";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const liveUpdateDelay = 100;
|
const liveUpdateDelay = 100;
|
||||||
const endLiveCount = 20;
|
const endLiveCount = 200;
|
||||||
const reportingUrl = `${process.env.QUALITEER_URL}/api/dev/rabbit/TestResults`;
|
const reportingUrl = `${process.env.QUALITEER_EXECUTOR_URL}/api/dev/rabbit/TestResults`;
|
||||||
// Pull args
|
// Pull args
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const test = (args.find((v) => v.includes("test=")) ?? "").replace("test=", "");
|
const test = (args.find((v) => v.includes("test=")) ?? "").replace("test=", "");
|
||||||
|
const isTriage = (args.find((v) => v.includes("isTriage=")) ?? "").replace(
|
||||||
|
"isTriage=",
|
||||||
|
""
|
||||||
|
);
|
||||||
var pipeline = (args.find((v) => v.includes("pipeline=")) ?? "").replace(
|
var pipeline = (args.find((v) => v.includes("pipeline=")) ?? "").replace(
|
||||||
"pipeline=",
|
"pipeline=",
|
||||||
""
|
""
|
||||||
|
@ -58,14 +63,26 @@ const runTests = () => {
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
liveIndicator();
|
liveIndicator();
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
const status = runTests();
|
const status = runTests();
|
||||||
if (pipeline) pipeline.testData = status.pipelineData;
|
if (pipeline) pipeline.testData = status.pipelineData;
|
||||||
const testResult = {
|
const testResult = {
|
||||||
...status,
|
...status,
|
||||||
|
failed: status.status !== 0,
|
||||||
name: test,
|
name: test,
|
||||||
pipeline,
|
pipeline,
|
||||||
|
triage: isTriage === "true",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*const rbt = new Rabbiteer(null, [], {
|
||||||
|
autoRabbit: { user: "guest", pass: "guest" },
|
||||||
|
});
|
||||||
|
await rbt.connect();
|
||||||
|
const data = JSON.stringify(testResult);
|
||||||
|
await rbt.publishMessageWithTimeout("TestResults", data);
|
||||||
|
rbt.disconnect();
|
||||||
|
if (status.status === 1) process.exit(1);*/
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(reportingUrl, { testResult })
|
.post(reportingUrl, { testResult })
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Initiator, Executor } from "qualiteer/clients";
|
||||||
const qltr = new Qualiteer();
|
const qltr = new Qualiteer();
|
||||||
await qltr.start();
|
await qltr.start();
|
||||||
|
|
||||||
const url = process.env.QUALITEER_URL;
|
const url = process.env.QUALITEER_EXECUTOR_URL;
|
||||||
|
|
||||||
// Create an initiator and make a job request
|
// Create an initiator and make a job request
|
||||||
const primary = new Initiator(url);
|
const primary = new Initiator(url);
|
||||||
|
|
|
@ -2,20 +2,21 @@ import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
|
const { QUALITEER_VITE_BACKEND_URL, QUALITEER_VITE_DEV_PORT } = process.env;
|
||||||
|
const backendUrl = QUALITEER_VITE_BACKEND_URL ?? "http://localhost:52000";
|
||||||
|
const vitePort = QUALITEER_VITE_DEV_PORT ?? 52025;
|
||||||
export default () => {
|
export default () => {
|
||||||
return defineConfig({
|
return defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
hmr: {
|
port: vitePort,
|
||||||
port: 443,
|
|
||||||
},
|
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://localhost:52000",
|
"/api": backendUrl,
|
||||||
"/socket.io": {
|
"/socket.io": backendUrl,
|
||||||
target: "ws://localhost:52000",
|
},
|
||||||
ws: true,
|
hmr: {
|
||||||
},
|
protocol: process.env.QUALITEER_VITE_DEV_PROTOCOL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
@ -26,10 +27,11 @@ export default () => {
|
||||||
alias: {
|
alias: {
|
||||||
"@qltr/util": path.resolve("./src/util/"),
|
"@qltr/util": path.resolve("./src/util/"),
|
||||||
"@qltr/queries": path.resolve("./src/util/queries"),
|
"@qltr/queries": path.resolve("./src/util/queries"),
|
||||||
|
"@qltr/joins": path.resolve(`./src/util/Joins.jsx`),
|
||||||
"@qltr/jobs": path.resolve("./src/ctx/JobContext.jsx"),
|
"@qltr/jobs": path.resolve("./src/ctx/JobContext.jsx"),
|
||||||
"@qltr/store": path.resolve("./src/ctx/StoreContext.jsx"),
|
"@qltr/store": path.resolve("./src/ctx/StoreContext.jsx"),
|
||||||
"@qltr/initiator": path.resolve("./lib/sockets/clients/Initiator.js"),
|
"@qltr/initiator": path.resolve("./lib/sockets/clients/Initiator.js"),
|
||||||
"@qltr/mocks": path.resolve("./lib/database/mocks/")
|
"@qltr/mocks": path.resolve("./lib/database/mocks/"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue