Savepoint

This commit is contained in:
Dunemask 2022-05-17 12:32:04 +00:00
parent 7db1a3456b
commit 02c483950c
45 changed files with 5136 additions and 256 deletions

3
.breakpoints Normal file
View file

@ -0,0 +1,3 @@
{
"files": {}
}

View file

@ -1,32 +1,38 @@
# Roadmap
### The end goal of this application is to provide a powerful application able to process and handle all of the your realtime QA data needs
## v0.0.1
- [x] Initial Skeleton
- [x] Frontend Drafts
- [ ] Frontend Core
- [ ] Frontend Pages
## v0.0.2
- [ ] Database Queries (Req PG)
- [ ] Database Tables (Req PG)
- [ ] Backend Routes (Req Database)
- [ ] Crons
## v0.0.3
- [ ] Rabbitmq Consumers (Req Database)
- [ ] Alerting
- [ ] Silencing
## v0.0.4
- [ ] Auth
## v0.0.5
- [ ] Docker config
- [ ] Gitlab Integration
- [ ] Garden config
## v0.0.6
- [ ] Internal Tests
- [ ] Self Test Suite

18
dev/query.js Normal file
View file

@ -0,0 +1,18 @@
import {
insertQuery,
selectWhereAnyQuery,
updateWhereAnyQuery,
} from "../lib/database/pg-query.js";
import { readFileSync } from "fs";
var data = JSON.parse(readFileSync("lib/routes/mocks/results.json"));
var table = "test_results";
var queries = data.results.map((r) => insertQuery(table, r));
queries.forEach((q) => console.log(q + ";"));
console.log();
table = "test_catalog";
data = JSON.parse(readFileSync("lib/routes/mocks/catalog.json"));
queries = data["tests:full"].map((r) => insertQuery(table, r));
queries.forEach((q) => console.log(q + ";"));

File diff suppressed because one or more lines are too long

View file

@ -6,11 +6,10 @@ const jobStr = process.argv.slice(2)[0];
const job = JSON.parse(jobStr);
const { command } = job.spec.template.spec.containers[0];
INFO("EXEC", "Internal Executor Starting!");
cp.exec(command, (error, stdout, stderr)=>{
if(error) ERR("EXEC", error);
//if(stdout) VERB("EXEC-STDOUT", stdout);
//if(stderr) VERB("EXEC-STDERR", stderr);
OK("EXEC", "Internal Executor Finished!");
process.exit(error? 1 : 0 );
cp.exec(command, (error, stdout, stderr) => {
if (error) ERR("EXEC", error);
//if(stdout) VERB("EXEC-STDOUT", stdout);
//if(stderr) VERB("EXEC-STDERR", stderr);
OK("EXEC", "Internal Executor Finished!");
process.exit(error ? 1 : 0);
});

View file

@ -5,9 +5,11 @@ import path from "path";
const internalDeploy = process.env.INTERNAL_DEPLOY === "true";
const executorUrl = process.env.EXECUTOR_URL;
const executorScriptOnly = process.env.EXECUTOR_SCRIPT_ONLY === "true";
const executorBin = process.env.EXECUTOR_BIN ?? `qltr-executor${executorScriptOnly ? ".js": ""}`;
const executorBin =
process.env.EXECUTOR_BIN ?? `qltr-executor${executorScriptOnly ? ".js" : ""}`;
const qualiteerUrl = process.env.QUALITEER_URL ?? "file:///home/runner/Qualiteer/bin/executor";
const qualiteerUrl =
process.env.QUALITEER_URL ?? "file:///home/runner/Qualiteer/bin/executor";
const kubCmd = "kubectl apply -f";
const jobsDir = "jobs/";
@ -16,11 +18,15 @@ const defaults = JSON.parse(
);
const wrapCommand = (jobId, command) => {
const bin = executorScriptOnly ? `node ${executorBin}`:`chmod +x ${executorBin} && ./${executorBin}`;
const cmd = command.map((arg)=>JSON.stringify(arg))
const curlCmd = `curl -o qltr-executor ${executorUrl} && ${bin} ${qualiteerUrl} ${jobId} ${cmd.join(" ")}`;
const bin = executorScriptOnly
? `node ${executorBin}`
: `chmod +x ${executorBin} && ./${executorBin}`;
const cmd = command.map((arg) => JSON.stringify(arg));
const curlCmd = `curl -o qltr-executor ${executorUrl} && ${bin} ${qualiteerUrl} ${jobId} ${cmd.join(
" "
)}`;
return curlCmd;
}
};
const createFile = (job) => {
const { name } = job.metadata;

View file

@ -5,11 +5,15 @@ import express from "express";
import results from "../routes/results-route.js";
import alerting from "../routes/alerting-route.js";
import react from "../routes/react-route.js";
import tests from "../routes/tests-route.js";
import catalog from "../routes/catalog-route.js";
import jobs from "../routes/jobs-route.js";
import mock from "../routes/mock-route.js";
const app = express();
// Special Routes
app.all("/", (req, res) => res.redirect("/qualiteer"));
if (process.env.MOCK_ROUTES === "true") app.use(mock);
// Middlewares
@ -17,7 +21,7 @@ app.all("/", (req, res) => res.redirect("/qualiteer"));
app.use(react); // Static Build Route
app.use("/api/results", results);
app.use("/api/alerting", alerting);
app.use("/api/tests", tests);
app.use("/api/catalog", catalog);
app.use("/api/jobs", jobs);
export default app;

View file

@ -1,25 +1,27 @@
CREATE SEQUENCE test_results_id_seq;
CREATE TABLE test_results (
id bigint NOT NULL DEFAULT nextval('test_results_seq') PRIMARY KEY,
test_name varchar(255) DEFAULT NULL,
test_class varchar(255) DEFAULT NULL,
test_method varchar(255) DEFAULT NULL,
test_path varchar(255) DEFAULT NULL,
test_type varchar(32) DEFAULT NULL,
test_timestamp timestamptz NOT NULL DEFAULT now(),
test_retry BOOLEAN DEFAULT FALSE,
origin varchar(255) DEFAULT NULL,
failed BOOLEAN DEFAULT FALSE,
failed_message varchar(2047) DEFAULT NULL,
screenshot_url varchar(255) DEFAULT NULL,
weblog_url varchar(255) DEFAULT NULL,
id bigint NOT NULL DEFAULT nextval('test_results_seq') PRIMARY KEY,
test_name varchar(255) DEFAULT NULL,
test_class varchar(255) DEFAULT NULL,
test_method varchar(255) DEFAULT NULL,
test_path varchar(255) DEFAULT NULL,
test_type varchar(32) DEFAULT NULL,
test_timestamp timestamptz NOT NULL DEFAULT now(),
test_retry BOOLEAN DEFAULT FALSE,
origin varchar(255) DEFAULT NULL,
failed BOOLEAN DEFAULT FALSE,
failed_message varchar(2047) DEFAULT NULL,
screenshot_url varchar(255) DEFAULT NULL,
weblog_url varchar(255) DEFAULT NULL,
);
ALTER SEQUENCE test_results_id_seq OWNED BY test_results.id;
# Tables
PG Database Tables Mapped Out
## ```test_results```
## `test_results`
| id | test_name | test_class | test_method | test_path | test_type | test_timestamp | test_retry | origin | failed | failed_message | screenshot_url | weblog_url |
| int | string | string | string | string | string | timestamp | boolean | string | boolean | string | string | string |
| 1 | My Test | My Test Class | My Failing Test Method | My Test Class Path | API | Date.now() | false | Test Suite A | true | Some Failure Messsage | screenshotUrl | weblogUrl |
@ -37,6 +39,3 @@ PG Database Tables Mapped Out
- failed_message Failure Message of test or null
- screenshot_url Screenshot of failure
- weblog_url Log from the web console

View file

@ -1,19 +1,14 @@
CREATE SEQUENCE test_results_id_seq;
CREATE TABLE test_results (
id bigint NOT NULL DEFAULT nextval('test_results_seq') PRIMARY KEY,
test_name varchar(255) DEFAULT NULL,
test_class varchar(255) DEFAULT NULL,
test_method varchar(255) DEFAULT NULL,
test_path varchar(255) DEFAULT NULL,
test_type varchar(32) DEFAULT NULL,
test_timestamp timestamptz NOT NULL DEFAULT now(),
test_retry BOOLEAN DEFAULT FALSE,
origin varchar(255) DEFAULT NULL,
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_url varchar(255) DEFAULT NULL,
weblog_url varchar(255) DEFAULT NULL,
screenshot varchar(255) DEFAULT NULL,
weblog varchar(255) DEFAULT NULL
);
ALTER SEQUENCE test_results_id_seq OWNED BY test_results.id;

View file

@ -0,0 +1,18 @@
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;

View file

@ -2,7 +2,7 @@
import { migrate } from "postgres-migrations";
import createPgp from "pg-promise";
import moment from "moment";
import { WARN } from "../util/logging.js";
import { INFO, WARN } from "../util/logging.js";
// Environment Variables
const {
POSTGRES_DATABASE: database,

View file

@ -12,4 +12,3 @@ export const getSilencedTests = () => {
const query = `SELECT * from ${table}`;
return pg.query(query);
};

View file

@ -12,4 +12,3 @@ export const getTests = () => {
const query = `SELECT * from ${table}`;
return pg.query(query);
};

View file

@ -0,0 +1,64 @@
import pg from "../postgres.js";
// Imports
import {
insertQuery,
selectWhereAnyQuery,
selectWhereAllQuery,
updateWhereAnyQuery,
} from "../pg-query.js";
// Constants
const table = "test_results";
// Queries
export const insertTestResult = (testResult) => {
const {
test_name,
test_class,
test_method,
test_path,
test_type,
test_timestamp,
test_retry,
origin,
failed,
failed_message,
screenshot_url,
expected_screenshot_url,
weblog_url,
} = testResult;
var query = insertQuery(table, {
test_name,
test_class,
test_method,
test_path,
test_type,
test_timestamp,
test_retry,
origin,
failed,
failed_message,
screenshot_url,
expected_screenshot_url,
weblog_url,
});
query += "\n RETURNING *";
return pg.query(query);
};
export const getCurrentlyFailing = () => {
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;
`;
return pg.query(query);
/*SELECT * FROM test_results WHERE "timestamp" BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW(); <-- Last 24 hours all runs*/
/* 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
*/
};
export const getCurrentlyFailingFull = (env) => {
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;
;`;
return pg.query(query);
};

View file

@ -1,51 +0,0 @@
import pg from "../postgres.js";
// Imports
import {
insertQuery,
selectWhereAnyQuery,
updateWhereAnyQuery,
} from "../pg-query.js";
// Constants
const table = "test_results";
// Queries
export const insertTestResult = (testResult) => {
const {
test_name,
test_class,
test_method,
test_path,
test_type,
test_timestamp,
test_retry,
origin,
failed,
failed_message,
screenshot_url,
expected_screenshot_url,
weblog_url,
} = testResult;
var query = insertQuery(table, {
test_name,
test_class,
test_method,
test_path,
test_type,
test_timestamp,
test_retry,
origin,
failed,
failed_message,
screenshot_url,
expected_screenshot_url,
weblog_url,
});
query += "\n RETURNING *";
return pg.query(query);
};
export const getCurrentlyFailing = () => {
const query = "SELECT *";
return pg.query(query);
};

View file

@ -1,5 +1,5 @@
import { Router, json as jsonMiddleware } from "express";
import { getSilencedTests } from "../database/queries/silenced_tests.js";
import { getSilencedTests } from "../database/queries/alerting.js";
const router = Router();
// Apply Middlewares
@ -11,7 +11,7 @@ router.get("/silenced", (req, res) => {
});
// Post Routes
router.post("/silence", (req,res)=>{
router.post("/silence", (req, res) => {
res.sendStatus(200);
});

View file

@ -1,11 +1,11 @@
import { Router, json as jsonMiddleware } from "express";
import { getTests } from "../database/queries/tests.js";
import { getTests } from "../database/queries/catalog.js";
const router = Router();
const maxSize = 1024 * 1024 * 100; // 100MB
// Apply Middlewares
router.use(jsonMiddleware({limit: maxSize}));
router.use(jsonMiddleware({ limit: maxSize }));
// Get Routes
router.get("/tests", async (req, res) => {
@ -14,7 +14,7 @@ router.get("/tests", async (req, res) => {
});
// Post Routes
router.post("/update", (req,res)=>{
router.post("/update", (req, res) => {
// Update All Tests
res.sendStatus(200);
});

View file

@ -6,7 +6,7 @@ const router = Router();
router.get("/jobs", (req, res) => {
const { clients } = jobs;
const allJobs = [];
for(var c of clients) allJobs.push(...c.jobs);
for (var c of clients) allJobs.push(...c.jobs);
res.json(allJobs);
});
export default router;

37
lib/routes/mock-route.js Normal file
View file

@ -0,0 +1,37 @@
import { Router } from "express";
import { readFileSync } from "fs";
const router = Router();
const catalog = "lib/routes/mocks/catalog.json";
const alerting = "lib/routes/mocks/alerting.json";
const results = "lib/routes/mocks/results.json";
const query = async (mock) => JSON.parse(readFileSync(mock));
// Queries
router.get("/api/catalog/tests", (req, res) => {
query(catalog).then((catalog) => {
res.json(req.get("full") ? catalog["tests:full"] : catalog.tests);
});
});
router.get("/api/results/failing", async (req, res) => {
query(results).then(async (results) => {
if (req.get("count")) res.json({ failing: results.results.length });
else if (!req.get("full")) res.json(results.results);
else
query(catalog).then((catalog) => {
res.json(
results.results.map((r) => ({
...catalog["tests:full"].find((t) => t.name === r.name),
...r,
}))
);
});
});
});
// Mutations
export default router;

View file

@ -0,0 +1,32 @@
{
"silenced": [
{
"name": "Test1",
"class": "TestClass1",
"method": "TestMethod1",
"regions": ["us"],
"alerting": "MyUtcDate"
},
{
"name": "Test2",
"class": null,
"method": "TestMethod1",
"regions": [],
"alerting": "MyUtcDate"
},
{
"name": "*",
"class": "*",
"method": "TestMethod1",
"regions": [],
"alerting": "MyUtcDate"
},
{
"name": "Test3",
"class": "TestClass3",
"method": "*",
"regions": ["us", "au"],
"alerting": "MyUtcDate"
}
]
}

View file

@ -0,0 +1,92 @@
{
"tests": [
{
"name": "Test 1",
"class": "Test Class 1",
"compound": false,
"type": "api"
},
{
"name": "Test 2",
"class": "Test Class 2",
"compound": false,
"type": "ui"
},
{
"name": "Test Primary",
"class": "Test Class Primary",
"compound": true,
"type": "api"
},
{
"name": "Test Secondary",
"class": "Test Class Secondary",
"compound": true,
"type": "ui"
}
],
"tests:full": [
{
"name": "Test1",
"class": "TestClass1",
"compound": false,
"type": "api",
"markers": ["Service1"],
"ignored": false,
"comment": "This Comment Is Part Of Test 1",
"coverage": ["/api/test1", "/api/test2"],
"env": ["prod", "ci"],
"path": "tests/api/test1.js",
"regions": ["us", "ca"],
"origin": "Repo1",
"cron": "1hour"
},
{
"name": "Test2",
"class": "TestClass2",
"compound": false,
"type": "ui",
"markers": ["Service2"],
"ignored": false,
"comment": "Comment belonging to Test2",
"coverage": ["Page1/FeatureA"],
"env": ["prod"],
"path": "tests/ui/test2.js",
"regions": [],
"origin": "Repo2",
"cron": "30min"
},
{
"name": "TestPrimary",
"class": "TestClassPrimary",
"compound": true,
"type": "api",
"markers": [
"ServiceComplex",
"compound_Repo2!TestClassSecondary#TestSecondary"
],
"ignored": false,
"comment": "Comment belonging to Test Primary",
"coverage": ["/api/compound"],
"env": ["ci"],
"path": "tests/api/primary.js",
"regions": [],
"origin": "Repo1",
"cron": "2hour"
},
{
"name": "TestSecondary",
"class": "TestClassSecondary",
"compound": true,
"type": "ui",
"markers": ["ServiceComplex"],
"ignored": false,
"coverage": ["PageComplex/FeatureA"],
"env": ["ci"],
"path": "tests/ui/secondary.js",
"regions": [],
"origin": "Repo2",
"cron": "2hour"
}
]
}

View file

@ -0,0 +1,48 @@
{
"results": [
{
"name": "Test1",
"method": "Test1Method",
"env": "prod",
"timestamp": "2022-05-10T16:35:27.220Z",
"retry": false,
"failed": true,
"failed_message": "Some failure message",
"screenshot": "https://example.com",
"weblog": "https://example.com"
},
{
"name": "Test2",
"method": "Test2Method",
"env": "prod",
"timestamp": "2022-05-10T16:35:31.682Z",
"retry": false,
"failed": true,
"failed_message": "Some failure message 2",
"screenshot": "https://example.com",
"weblog": "https://example.com"
},
{
"name": "Test1",
"method": null,
"env": "prod",
"timestamp": "2022-05-10T16:35:33.810Z",
"retry": false,
"failed": false,
"failed_message": null,
"screenshot": "https://example.com",
"weblog": "https://example.com"
},
{
"name": "Test1",
"method": null,
"env": "ci",
"timestamp": "2022-05-10T16:35:33.810Z",
"retry": false,
"failed": false,
"failed_message": null,
"screenshot": "https://example.com",
"weblog": "https://example.com"
}
]
}

View file

@ -1,5 +1,5 @@
import { Router, json as jsonMiddleware } from "express";
import { getCurrentlyFailing } from "../database/queries/test_results.js";
import { getCurrentlyFailing } from "../database/queries/results.js";
const router = Router();
// Apply Middlewares
@ -11,7 +11,7 @@ router.get("/failing", (req, res) => {
});
// Post Routes
router.post("/history", (req,res)=>{
router.post("/history", (req, res) => {
res.send([]);
});

View file

@ -67,7 +67,8 @@ export default class Executor {
report(d, dType) {
this.buf[dType] += d;
if (!this.buf[dType].includes("\n")) return;
if(this.buf[dType].endsWith("\n")) this.buf[dType] = this.buf[dType].slice(0, -1);
if (this.buf[dType].endsWith("\n"))
this.buf[dType] = this.buf[dType].slice(0, -1);
this.socket.emit(events.JOB_REP, this.buf[dType]);
if (dType === ERR) console.error(`err: ${this.buf[dType]}`);
else console.log(`out: ${this.buf[dType]}`);

134
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "qualiteer",
"version": "1.0.0",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "qualiteer",
"version": "1.0.0",
"version": "0.0.1",
"license": "LGPL-2.1-only",
"dependencies": {
"amqplib": "^0.8.0",
@ -18,12 +18,13 @@
"path": "^0.12.7",
"pg-promise": "^10.11.1",
"postgres-migrations": "^5.3.0",
"react-router-dom": "^6.3.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"uuid": "^8.3.2"
},
"bin": {
"qualiteer": "bin/app.js"
"qualiteer": "dist/app.js"
},
"devDependencies": {
"@emotion/react": "^11.9.0",
@ -33,6 +34,7 @@
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"axios": "^0.27.2",
"caxa": "^2.1.0",
"nodemon": "^2.0.15",
"react": "^18.1.0",
@ -1904,7 +1906,6 @@
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz",
"integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
@ -5160,6 +5161,30 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -9674,6 +9699,14 @@
"he": "bin/he"
}
},
"node_modules/history": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dependencies": {
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -12764,8 +12797,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "3.14.1",
@ -13173,7 +13205,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -15814,7 +15845,6 @@
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz",
"integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -16012,7 +16042,6 @@
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz",
"integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.22.0"
@ -16042,6 +16071,30 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dependencies": {
"history": "^5.2.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dependencies": {
"history": "^5.2.0",
"react-router": "6.3.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -16407,8 +16460,7 @@
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/regenerator-transform": {
"version": "0.15.0",
@ -16913,7 +16965,6 @@
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz",
"integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -21090,7 +21141,6 @@
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz",
"integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -23495,6 +23545,29 @@
"integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==",
"dev": true
},
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -26887,6 +26960,14 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"history": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -29151,8 +29232,7 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.14.1",
@ -29495,7 +29575,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
@ -31312,7 +31391,6 @@
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz",
"integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0"
}
@ -31455,7 +31533,6 @@
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz",
"integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"scheduler": "^0.22.0"
@ -31479,6 +31556,23 @@
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
"dev": true
},
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": {
"history": "^5.2.0"
}
},
"react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": {
"history": "^5.2.0",
"react-router": "6.3.0"
}
},
"react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@ -31727,8 +31821,7 @@
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"regenerator-transform": {
"version": "0.15.0",
@ -32072,7 +32165,6 @@
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz",
"integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0"
}

View file

@ -25,6 +25,7 @@
"start:react": "react-scripts start",
"start:react:replit": "DANGEROUSLY_DISABLE_HOST_CHECK=true npm run start:react",
"test": "node tests/index.js",
"test:api": "node tests/api.js",
"test:dev": "nodemon tests/index.js"
},
"browserslist": {
@ -49,6 +50,7 @@
"path": "^0.12.7",
"pg-promise": "^10.11.1",
"postgres-migrations": "^5.3.0",
"react-router-dom": "^6.3.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"uuid": "^8.3.2"
@ -61,6 +63,7 @@
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"axios": "^0.27.2",
"caxa": "^2.1.0",
"nodemon": "^2.0.15",
"react": "^18.1.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
public/assets/QA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -6,7 +6,7 @@ export default {
input: "lib/core/executor.js",
output: {
file: "dist/bundles/qualiteer-executor.js",
format: "cjs"
format: "cjs",
},
plugins: [nodeResolve(), commonjs(), terser()],
};

View file

@ -1,10 +1,7 @@
import { useContext } from "react";
// Import Contexts
import { JobProvider } from "./ctx/JobContext.jsx";
import { ViewProvider } from "./ctx/ViewContext.jsx";
import { StoreProvider } from "./ctx/StoreContext.jsx";
import { BrowserRouter } from "react-router-dom";
// Import Views
import Views from "./Views.jsx";
@ -13,9 +10,9 @@ export default function Dashboard() {
<div className="qualiteer">
<JobProvider>
<StoreProvider>
<ViewProvider>
<BrowserRouter>
<Views />
</ViewProvider>
</BrowserRouter>
</StoreProvider>
</JobProvider>
</div>

View file

@ -1,21 +1,23 @@
import { useContext, useState } from "react";
import ViewContext from "./ctx/ViewContext.jsx";
import * as React from "react";
import {
Routes,
Route,
Link,
BrowserRouter,
Navigate,
useLocation,
} from "react-router-dom";
import AppBar from "@mui/material/AppBar";
import Badge, { BadgeProps } from "@mui/material/Badge";
import { styled } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import MenuIcon from "@mui/icons-material/Menu";
import Container from "@mui/material/Container";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
@ -24,48 +26,68 @@ import NotificationsIcon from "@mui/icons-material/Notifications";
import WorkIcon from "@mui/icons-material/Work";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import SettingsIcon from "@mui/icons-material/Settings";
import ErrorIcon from "@mui/icons-material/Error";
import WarningIcon from "@mui/icons-material/Warning";
import InfoIcon from "@mui/icons-material/Info";
// Import Pages
import Failing from "./views/Failing.jsx";
import Alerting from "./views/Alerting.jsx";
import Jobs from "./views/Jobs.jsx";
import Catalog from "./views/Catalog.jsx";
import Settings from "./views/Settings.jsx";
import About from "./views/About.jsx";
const pages = ["failing", "alerting", "jobs", "tests", "settings"];
const pages = ["failing", "alerting", "jobs", "catalog", "settings", "about"];
const icons = [
ErrorIcon,
NotificationsIcon,
WorkIcon,
FormatListBulletedIcon,
SettingsIcon,
<Badge badgeContent={4} color="error">
<WarningIcon />
</Badge>,
<NotificationsIcon />,
<Badge badgeContent={4} color="primary">
<WorkIcon />
</Badge>,
<FormatListBulletedIcon />,
<SettingsIcon />,
<InfoIcon />,
];
const drawerWidth = 240;
export default function Views() {
const [view, setView] = useState(pages[0]);
const location = useLocation();
const [drawerOpen, setDrawer] = React.useState(false);
const toggleDrawer = () => setDrawer(!drawerOpen);
const closeDrawer = () => setDrawer(false);
const openPage = (e) => setView(e.target.outerText.toLowerCase());
const reloadPage = () => window.location.reload(false);
const SideBadge = styled(Badge)(({ theme }) => ({
"& .MuiBadge-badge": {
right: -6,
top: 10,
padding: "0 4px",
},
}));
const navHeader = () => {
const pathStr =
location.pathname.charAt(1).toUpperCase() + location.pathname.slice(2);
if (location.pathname !== "/failing") return pathStr;
return (
<SideBadge badgeContent={4} color="error" overlap="circular">
{pathStr}
</SideBadge>
);
};
return (
<AppBar position="static" color="secondary">
<Container maxWidth="xl">
<Toolbar disableGutters>
<Drawer open={drawerOpen} onClose={closeDrawer}>
{" "}
<Box sx={{ width: 250 }} role="presentation">
<List>
{pages.map((text, index) => (
<ListItemButton
key={text}
onClick={openPage}
selected={view === text}
<div className="view">
<AppBar
position="fixed"
sx={{ bgcolor: "black", zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<ListItemIcon>{/*icons[index]*/}</ListItemIcon>
<ListItemText
primary={text.charAt(0).toUpperCase() + text.slice(1)}
/>
</ListItemButton>
))}
</List>
</Box>
</Drawer>
<Box sx={{ flexGrow: 1, margin: "0 20px" }}>
<Toolbar disableGutters>
<IconButton
size="large"
edge="start"
@ -76,27 +98,52 @@ export default function Views() {
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
noWrap
component="div"
sx={{ mr: 2, display: { xs: "none", md: "flex" } }}
<Drawer open={drawerOpen} onClose={closeDrawer}>
<Toolbar />
<Box sx={{ width: 250, overflow: "auto" }} role="presentation">
<List>
{pages.map((text, index) => (
<ListItemButton
key={text}
component={Link}
to={"/" + text}
selected={location.pathname === "/" + text}
onClick={closeDrawer}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</Typography>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: "flex", md: "none" } }}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</Typography>
<Box sx={{ flexGrow: 0 }}>
<Avatar alt="Remy Sharp" src="/assets/QA.jpg" />
<ListItemIcon>{icons[index]}</ListItemIcon>
<ListItemText
primary={text.charAt(0).toUpperCase() + text.slice(1)}
/>
</ListItemButton>
))}
</List>
</Box>
</Drawer>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1 }}
>
{navHeader()}
</Typography>
<Avatar alt="Remy Sharp" src="/assets/QA.png" onClick={reloadPage}/>
</Toolbar>
</Container>
</Box>
</AppBar>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
<Routes>
<Route exact path="/" element={<Navigate to="/failing" replace />} />
<Route path="/failing" element={<Failing />} />
<Route path="/alerting" element={<Alerting />} />
<Route path="/jobs" element={<Jobs />} />
<Route path="/catalog" element={<Catalog />} />
<Route path="/settings" element={<Settings pages={pages} />} />
<Route path="/about" element={<About />} />
</Routes>
</Box>
</div>
);
}

View file

@ -36,16 +36,32 @@ const reducer = (state, action) => {
}
};
export const JobProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const jobUpdate = (job, jobId) => dispatch({ type: ACTIONS.UPDATE, jobId, job });
const jobCreate = (job) =>
dispatch({ type: ACTIONS.CREATE, job: { ...job, log: [] } });
const jobDelete = (jobId) => dispatch({ type: ACTIONS.DELETE, jobId });
function retryAll(failing){
// Query Full Locator
console.log("Would retry all failing tests!");
}
function jobBuilder(){
}
const context = {
state,
dispatch,
jobUpdate: (job, jobId) => dispatch({ type: ACTIONS.UPDATE, jobId, job }),
jobCreate: (job) =>
dispatch({ type: ACTIONS.CREATE, job: { ...job, log: [] } }),
jobDelete: (jobId) => dispatch({ type: ACTIONS.DELETE, jobId }),
jobUpdate,
jobCreate,
jobDelete,
retryAll
};
const contextValue = useMemo(() => context, [state, dispatch]);

View file

@ -5,9 +5,18 @@ const ACTIONS = {
UPDATE: "u",
};
const initialState = {};
const initialState = {
intervals: [],
failing: [],
regions: [],
focusJob: false,
simplifiedControls: false,
defaultRegion: "us", // Local Store
defaultPage: "failing", // Local Store
};
const reducer = (state, action) => {
const { store } = action;
// Actions
switch (action.type) {
case ACTIONS.UPDATE:
@ -23,7 +32,7 @@ export const StoreProvider = ({ children }) => {
const context = {
state,
dispatch,
updateStore: (store) => dispatch(state, { type: ACTIONS.UPDATE, store }),
updateStore: (store) => dispatch({ type: ACTIONS.UPDATE, store }),
};
const contextValue = useMemo(() => context, [state, dispatch]);

View file

@ -1,32 +0,0 @@
import React, { useReducer, createContext, useMemo } from "react";
const ViewContext = createContext();
const ACTIONS = {
UPDATE: "u",
};
const initialState = {
activePage: "Home",
};
const reducer = (state, action) => {
// Actions
switch (action.type) {
case ACTIONS.UPDATE:
return { ...state };
default:
return state;
}
};
export const ViewProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const contextValue = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return (
<ViewContext.Provider value={contextValue}>{children}</ViewContext.Provider>
);
};
export default ViewContext;

32
src/views/About.jsx Normal file
View file

@ -0,0 +1,32 @@
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
const memeUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
const repoUrl = "https://gitlab.com/dunemask/Qualiteer";
export default function About() {
return (
<div className="about">
<Container maxWidth="sm">
<Typography variant="h6" gutterBottom component="div">
<Box fontWeight='bold' display='inline'>Why?</Box>
</Typography>
<Typography variant="body1">
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 what is broken in production! 🤯
Qualiteer gives users power to resolve and reattempt failing tests, run a particular suite of tests, and mute pesky alerts reminding you the navbar's color changed... 🤦
</Typography>
<br/>
<Typography variant="subtitle1" style={{ wordWrap: "break-word", whiteSpace:"normal" }}>
<Box fontWeight='bold' display='inline'>{"Repository: "} </Box>
<Link href={repoUrl} >{repoUrl}</Link>
</Typography>
<br/>
<div style={{justifyContent:"center", width:"100%", display:"flex"}}>
<Link href={memeUrl} variant="h6" underline="none">Qualiteer</Link>
</div>
</Container>
</div>
);
}

61
src/views/Alerting.jsx Normal file
View file

@ -0,0 +1,61 @@
import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
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';
export default function Alerting() {
const { state: store, updateStore } = useContext(StoreContext);
const [alertDialogOpen, setAlertDialogOpen] = useState(false);
const quickAlertClick = () => setAlertDialogOpen(!alertDialogOpen);
function silenceAlert(){
}
const handleClose = (confirmed) => () => {
quickAlertClick();
if(!confirmed) return;
silenceAlert();
}
return (
<div className="alerting">
<Dialog
open={alertDialogOpen}
onClose={handleClose()}
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
>
<DialogTitle>
Silence Alert
</DialogTitle>
<DialogContent>
</DialogContent>
<DialogActions>
<Button onClick={handleClose()}>Cancel</Button>
<Button onClick={handleClose(true)} autoFocus>
Silence
</Button>
</DialogActions>
</Dialog>
<SpeedDial
ariaLabel="Silence Alert"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickAlertClick}
open={false}
/>
</div>
);
}

29
src/views/Catalog.jsx Normal file
View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import TextField from "@mui/material/TextField";
import CatalogSearch from "./components/CatalogSearch.jsx";
export default function Catalog() {
const {
state: jobState,
dispatch: jobDispatch,
jobUpdate,
jobCreate,
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
return (
<div className="catalog">
<CatalogSearch />
<TextField
label="Search Catalog"
type="search"
variant="filled"
/>
</div>
);
}

70
src/views/Failing.jsx Normal file
View file

@ -0,0 +1,70 @@
import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
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';
export default function Failing() {
const {
state: jobState,
retryAll
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const [retryAllOpen, setRetryAllOpen] = useState(false);
const retryAllClick = () => setRetryAllOpen(!retryAllOpen);
const handleClose = (confirmed) => ()=> {
retryAllClick();
if(!confirmed) return;
retryAll(store.failing);
}
return (
<div className="failing">
<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>
<SpeedDial
ariaLabel="Retry All"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<ReplayIcon />}
onClick={retryAllClick}
open={false}
/>
</div>
);
}

55
src/views/Jobs.jsx Normal file
View file

@ -0,0 +1,55 @@
import { useState, useContext } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import JobContext from "../ctx/JobContext.jsx";
import ClickAwayListener from '@mui/material/ClickAwayListener';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import SpeedDialIcon from '@mui/material/SpeedDialIcon';
import PageviewIcon from '@mui/icons-material/Pageview';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import ViewCarouselIcon from '@mui/icons-material/ViewCarousel';
export default function Jobs() {
const {
state: jobState,
dispatch: jobDispatch,
jobUpdate,
jobCreate,
} = useContext(JobContext);
const { state: store, updateStore } = useContext(StoreContext);
const [quickOpen, setQuickOpen] = useState(false);
const quickOpenClick = () => setQuickOpen(!quickOpen);
const quickOpenClose = () => setQuickOpen(false);
const actions = [
{name: "Suite", icon: <ViewCarouselIcon/>}, {name: "Compound", icon: <ViewColumnIcon/>}, {name: "Manual", icon: <PageviewIcon/>}
]
return (
<div className="jobs">
<ClickAwayListener onClickAway={quickOpenClose}>
<SpeedDial
ariaLabel="New Job"
sx={{ position: 'absolute', bottom: 16, right: 16 }}
icon={<SpeedDialIcon />}
onClick={quickOpenClick}
open={quickOpen}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
/>
))}
</SpeedDial>
</ClickAwayListener>
</div>
);
}

124
src/views/Settings.jsx Normal file
View file

@ -0,0 +1,124 @@
import { useContext, useState, useEffect } from "react";
import StoreContext from "../ctx/StoreContext.jsx";
import MultiOptionDialog from "./components/MultiOptionDialog.jsx";
import * as React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from "@mui/material/Switch";
import SummarizeIcon from '@mui/icons-material/Summarize';
import Typography from "@mui/material/Typography";
export default function Settings(props) {
const { state: store, updateStore } = useContext(StoreContext);
const { regions } = store;
const { pages } = props;
const defaultDialog = {title: "", options: [], current: null, onSelect: null, open: false};
const [dialog, setDialog] = React.useState(defaultDialog);
const optionSettings = {region: {
title: "Region",
options: ["us", "au"],
current: store.defaultRegion,
onSelect: (r) => updateStore({defaultRegion: r})
},
defaultPage: {
title: "Default Page",
options: ["failing", "alerting"],
current: store.defaultPage,
onSelect: (p) => updateStore({defaultPage: p})
}}
const handleOptionsMenu = (s) => {
setDialog({...s, open:true});
};
const handleClose = (newValue, onSelect) => {
setDialog({...dialog, open:false})
if (!newValue) return;
onSelect(newValue);
};
const handleToggle = (booleanSetting) => ()=> {
const storeUpdate = {};
storeUpdate[booleanSetting] = !store[booleanSetting];
updateStore(storeUpdate)
}
function MultiOptionSubtext(props){
return( <React.Fragment>
<Typography
sx={{ display: 'inline' }}
component="span"
variant="body2"
color="primary"
>
{props.value}
</Typography>
</React.Fragment>)
}
return (
<Box sx={{ width: '100%', bgcolor: 'background.paper' }}>
<List component="div" role="group">
<ListItem
button
divider
aria-haspopup="true"
aria-label="default page"
onClick={() => handleOptionsMenu(optionSettings.defaultPage)}
>
<ListItemText primary="Default Page" secondary={
<MultiOptionSubtext value={optionSettings.defaultPage.current} />
}/>
</ListItem>
<ListItem
button
divider
aria-haspopup="true"
aria-label="region"
onClick={() => handleOptionsMenu(optionSettings.region)}
>
<ListItemText primary="Region" secondary={<MultiOptionSubtext value={optionSettings.region.current} />} />
</ListItem>
<ListItem button divider>
<ListItemText primary="Simplified Controls" />
<Switch
edge="end"
onChange={handleToggle("simplifiedControls")}
checked={store.simplifiedControls}
/>
</ListItem>
<ListItem button divider>
<ListItemText primary="Focus New Jobs" />
<Switch
edge="end"
onChange={handleToggle("focusJob")}
checked={store.focusJob}
/>
</ListItem>
<MultiOptionDialog
id="multi-options-menu"
keepMounted
open={dialog.open}
onClose={handleClose}
dialog={dialog}
/>
</List>
</Box>
);
}

View file

@ -0,0 +1,31 @@
import * as React from 'react';
import Paper from '@mui/material/Paper';
import InputBase from '@mui/material/InputBase';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import DirectionsIcon from '@mui/icons-material/Directions';
import ClearOutlinedIcon from "@mui/icons-material/ClearOutlined";
export default function SearchBar(props) {
return (
<Paper
component="form"
sx={{ display: 'flex', alignItems: 'center'}}
>
<InputBase
sx={{flex: 1 }}
placeholder="Search Catalog"
inputProps={{ 'aria-label': `search catalog` }}
/>
<IconButton type="submit" sx={{ p: '18px' }} aria-label="search">
<SearchIcon />
</IconButton>
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
<IconButton sx={{ p: '8px' }} aria-label="clear">
<ClearOutlinedIcon />
</IconButton>
</Paper>
);
}

View file

@ -0,0 +1,73 @@
import {useState, useRef, useEffect} from "react";
import Button from "@mui/material/Button"
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControlLabel from '@mui/material/FormControlLabel';
export default function MultiOptionDialog(props) {
const { dialog: dialogProp, onClose, open, ...other } = props;
const [value, setValue] = useState(dialogProp.current);
const [dialog, setDialog] = useState(dialogProp);
const radioGroupRef = useRef(null);
useEffect(() => {
setDialog(dialogProp);
setValue(dialogProp.current);
}, [dialogProp, open]);
const handleEntering = () => {
if (radioGroupRef.current != null) radioGroupRef.current.focus();
};
const handleCancel = () => onClose();
const handleOk = () => onClose(value, dialog.onSelect);
const handleChange = (e) =>{ setValue(e.target.value);
}
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
TransitionProps={{ onEntering: handleEntering }}
open={open}
{...other}
>
<DialogTitle>{dialog.title}</DialogTitle>
<DialogContent dividers>
<RadioGroup
ref={radioGroupRef}
aria-label={dialogProp.title}
name={dialog.title}
value={value}
onChange={handleChange}
>
{dialog.options.map((option) => (
<FormControlLabel
value={option}
key={option}
control={<Radio />}
label={option}
/>
))}
</RadioGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleOk}>Ok</Button>
</DialogActions>
</Dialog>
);
}

22
tests/api.js Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env node
import "dotenv/config"; // Load Envars
import Qualiteer from "qualiteer";
import axios from "axios";
// Start server
const qltr = new Qualiteer();
await qltr.start();
const url = "https://Qualiteer.elijahparker3.repl.co";
const testsUrl = "/api/catalog/tests";
const resultsUrl = "/api/results/failing";
const get = (...args) => axios.get(`${url}/${args[0]}`, args[1]);
var res = await get(resultsUrl);
console.log(res.data);
res = await get(resultsUrl, { headers: { count: true } });
console.log(res.data);

View file

@ -11,7 +11,11 @@ const url = "https://Qualiteer.elijahparker3.repl.co";
// Create an initiator and make a job request
const primary = new Initiator(url);
const job = { command: ["node", "dev/other.js"], name: "testing", image: "node" };
const job = {
command: ["node", "dev/other.js"],
name: "testing",
image: "node",
};
await primary.newJob(job, null, () => console.log("Primary Job Concluded"));
/*const { clients } = qltr.jobs;