diff --git a/.eslintrc b/.eslintrc index 3bf36364..52083e88 100644 --- a/.eslintrc +++ b/.eslintrc @@ -63,6 +63,7 @@ "space-infix-ops":"warn", "space-before-blocks": "warn", "keyword-spacing": "warn", + "key-spacing": 1, "strict": 1, "comma-dangle": 1, "triple-equals": 0, diff --git a/.gitignore b/.gitignore index c4aba493..882a8fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ testmerge.json .env.development.local* .env.test.local* .env.production.local* - +.DS_Store diff --git a/agent/test/pewpewtest.spec.ts b/agent/test/pewpewtest.spec.ts index 17b6cb2c..2b1ab17f 100644 --- a/agent/test/pewpewtest.spec.ts +++ b/agent/test/pewpewtest.spec.ts @@ -190,7 +190,7 @@ describe("PewPewTest", () => { endTime: now + 2, resultsFilename: ["test2"], status: TestStatus.Failed, - version:"version3", + version: "version3", userId: "user3" }; diff --git a/controller/components/PewPewVersions/index.tsx b/controller/components/PewPewVersions/index.tsx index 414188e7..23d1c035 100644 --- a/controller/components/PewPewVersions/index.tsx +++ b/controller/components/PewPewVersions/index.tsx @@ -1,6 +1,7 @@ import { Danger } from "../Alert"; import Div from "../Div"; import React from "react"; +import Toaster from "../Toaster"; import styled from "styled-components"; const VersionDiv = styled(Div)` @@ -24,6 +25,7 @@ const VersionDivSelect = styled(Div)` export interface VersionInitalProps { pewpewVersion: string; pewpewVersions: string[]; + latestPewPewVersion: string; loading: boolean; error: boolean; } @@ -38,12 +40,14 @@ export const PewPewVersions = ({ name, pewpewVersion, onChange, + latestPewPewVersion: latestPewPewTag, pewpewVersions = [], loading, error }: VersionProps) => { // console.log("PewPewVersions state", { pewpewVersions, loading, error }); let optionItems: JSX.Element[] | undefined; + const toasterMessage: string = "IMPORTANT: Please configure your YAML properly!!! The latest version of Pewpew is set to: " + latestPewPewTag; if (pewpewVersions && pewpewVersions.length > 0) { optionItems = pewpewVersions.map((version: string) => ()); } @@ -53,6 +57,7 @@ export const PewPewVersions = ({ {loading && Loading...} {!loading && !error && } {error && Could not load the current PewPew Versions} + ); }; diff --git a/controller/components/PewPewVersions/initialProps.ts b/controller/components/PewPewVersions/initialProps.ts index d2f81f8d..38f301ff 100644 --- a/controller/components/PewPewVersions/initialProps.ts +++ b/controller/components/PewPewVersions/initialProps.ts @@ -1,12 +1,13 @@ import { LogLevel, log } from "@fs/ppaas-common"; +import { getCurrentPewPewLatestVersion, getPewPewVersionsInS3 } from "../../pages/api/util/pewpew"; import { API_PEWPEW } from "../../types"; import { VersionInitalProps } from "."; -import { getPewPewVersionsInS3 } from "../../pages/api/util/pewpew"; import { latestPewPewVersion } from "../../pages/api/util/clientutil"; export const getServerSideProps = async (): Promise => { try { const pewpewVersions: string[] = await getPewPewVersionsInS3(); + const currentPewPewLatestVersion: string | undefined = await getCurrentPewPewLatestVersion(); log("getPewPewVersionsInS3", LogLevel.DEBUG, pewpewVersions); // Grab the response // console.log("PewPewVersions pewpewVersions: " + JSON.stringify(pewpewVersions), pewpewVersions); @@ -16,6 +17,7 @@ export const getServerSideProps = async (): Promise => { return { pewpewVersion: latestPewPewVersion, // We always want to default to latest pewpewVersions, + latestPewPewVersion: currentPewPewLatestVersion || "unknown", loading: false, error: false }; @@ -25,6 +27,7 @@ export const getServerSideProps = async (): Promise => { return { pewpewVersion: "", pewpewVersions: [], + latestPewPewVersion: "unknown", loading: false, error: true }; diff --git a/controller/components/PewPewVersions/story.tsx b/controller/components/PewPewVersions/story.tsx index 68d7be34..42c4b011 100644 --- a/controller/components/PewPewVersions/story.tsx +++ b/controller/components/PewPewVersions/story.tsx @@ -15,11 +15,12 @@ const props: VersionProps = { console.log("newVal: ", newVal); }, pewpewVersions: ["0.1.1", "0.1.2", "0.1.3", "0.1.4", "0.1.5", latestPewPewVersion, "0.1.6"], + latestPewPewVersion: "0.1.6", loading: false, error: false }; -const propsLoading: VersionProps = { ...props, pewpewVersions: [], loading: true }; -const propsError: VersionProps = { ...props, error: true }; +const propsLoading: VersionProps = { ...props, pewpewVersions: [], latestPewPewVersion: "unknown", loading: true }; +const propsError: VersionProps = { ...props, error: true, latestPewPewVersion: "unknown" }; export default { title: "PewPewVersions" diff --git a/controller/components/StartTestForm/index.tsx b/controller/components/StartTestForm/index.tsx index 9734d1ec..67bdc8a4 100644 --- a/controller/components/StartTestForm/index.tsx +++ b/controller/components/StartTestForm/index.tsx @@ -174,7 +174,7 @@ export const StartTestForm = ({ allDays: defaultDaysOfWeek.every((dayOfWeek: DayValue) => dayOfWeek.value), queueName: previousTestData?.queueName || queueInitialProps.queueName, pewpewVersion: previousTestData?.version || (recurringTest ? maxPewPewVersion : versionInitalProps.pewpewVersion), - environmentVariables: previousTestData?.environmentVariables + environmentVariables: previousTestData?.environmentVariables ? Object.entries(previousTestData.environmentVariables).map(([variableName, variableValue]: [string, string | null], index: number) => ({ // Map these to the placeholder values name: "previousVar" + index, // We can't use the date or we'll get constant redraws, use the index diff --git a/controller/components/StartTestForm/story.tsx b/controller/components/StartTestForm/story.tsx index b557e282..18560c4e 100644 --- a/controller/components/StartTestForm/story.tsx +++ b/controller/components/StartTestForm/story.tsx @@ -38,6 +38,7 @@ const versionInitalPropsEmpty: VersionInitalProps = { pewpewVersion: "", loading: false, pewpewVersions: [], + latestPewPewVersion: "unknown", error: true }; const queueInitialProps: QueueInitialProps = { @@ -50,6 +51,7 @@ const versionInitalProps: VersionInitalProps = { pewpewVersion: "", loading: false, pewpewVersions: [latestPewPewVersion, "0.5.10", "0.5.11", "0.5.12"], + latestPewPewVersion: "0.5.12", error: false }; let ppaasTestId: PpaasTestId; diff --git a/controller/components/TestInfo/index.tsx b/controller/components/TestInfo/index.tsx index db4e42d6..22efcd6a 100644 --- a/controller/components/TestInfo/index.tsx +++ b/controller/components/TestInfo/index.tsx @@ -54,7 +54,7 @@ export const TestInfo = ({ testData, ...testInfoProps }: TestInfoProps) => { const defaultState: TestInfoState = { message: testInfoProps.message || undefined, messageId: testInfoProps.messageId || undefined, - killTest : testInfoProps.killTest || undefined, + killTest: testInfoProps.killTest || undefined, error: testInfoProps.error || undefined }; const [state, setState] = useState(defaultState); diff --git a/controller/components/Toaster/index.tsx b/controller/components/Toaster/index.tsx new file mode 100644 index 00000000..77b14fce --- /dev/null +++ b/controller/components/Toaster/index.tsx @@ -0,0 +1,49 @@ +import React, { useEffect } from "react"; +import styled, { keyframes } from "styled-components"; +import { Info } from "../Alert"; + +interface ToasterProps { + message: string; // Ensure message prop is of type string + duration?: number; + id: string; // Unique ID for the toaster +} + +// Fade-in animation +const fadeIn = keyframes` + from { opacity: 0; } + to { opacity: 1; } +`; + +// Fade-out animation +const fadeOut = keyframes` + from { opacity: 1; } + to { opacity: 0; } +`; + +const Container = styled(Info)` + position: fixed; + bottom: 20px; + right: 20px; + width: 30%; + font-size: 17px; + + animation: ${fadeIn} 0.3s ease-in-out forwards; + &.fade-out { animation: ${fadeOut} 0.3s ease-in-out forwards; } +`; + +const Toaster: React.FC = ({ id, message, duration = 7000 }) => { + useEffect(() => { + setTimeout(() => { + const toasterElement = document.getElementById(id); + if (toasterElement) { + toasterElement.classList.add("fade-out"); + setTimeout(() => { + toasterElement.remove(); + }, 300); // Wait for fade-out animation to complete before removing element + } + }, duration); + }, [id, duration]); + return {message}; +}; + +export default Toaster; diff --git a/controller/components/Toaster/story.tsx b/controller/components/Toaster/story.tsx new file mode 100644 index 00000000..d94ca9f2 --- /dev/null +++ b/controller/components/Toaster/story.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryFn } from "@storybook/react"; +import { GlobalStyle } from "../Layout"; +import React from "react"; +import Toaster from "./index"; + +export default { + title: "Toaster", + component: Toaster +} as Meta; + +export const Default: StoryFn = () => ( + + + + +); diff --git a/controller/integration/pewpew.spec.ts b/controller/integration/pewpew.spec.ts index 339d239c..5a1bc0cb 100644 --- a/controller/integration/pewpew.spec.ts +++ b/controller/integration/pewpew.spec.ts @@ -1,8 +1,8 @@ import { AuthPermission, AuthPermissions, ErrorResponse, PewPewVersionsResponse, TestManagerError } from "../types"; import type { File, Files } from "formidable"; -import { LogLevel, log, logger } from "@fs/ppaas-common"; -import { ParsedForm, createFormidableFile, unzipFile } from "../pages/api/util/util"; -import { deletePewPew, getPewPewVersionsInS3, getPewpew, postPewPew } from "../pages/api/util/pewpew"; +import { LogLevel, PpaasS3File, log, logger } from "@fs/ppaas-common"; +import { PEWPEW_BINARY_FOLDER, ParsedForm, createFormidableFile, unzipFile } from "../pages/api/util/util"; +import { VERSION_TAG_NAME, deletePewPew, getCurrentPewPewLatestVersion, getPewPewVersionsInS3, getPewpew, postPewPew } from "../pages/api/util/pewpew"; import { expect } from "chai"; import { latestPewPewVersion } from "../pages/api/util/clientutil"; import path from "path"; @@ -22,6 +22,7 @@ const authAdmin: AuthPermissions = { let sharedPewPewVersions: string[] | undefined; let uploadedPewPewVersion: string | undefined; // Used for delete +let currentPewPewLatestVersion: string | undefined; describe("PewPew Util Integration", () => { let filesLegacyPewpew: Files = {}; @@ -101,7 +102,7 @@ describe("PewPew Util Integration", () => { files: filesLegacyPewpew }; log("postPewPew parsedForm", LogLevel.DEBUG, parsedForm); - postPewPew(parsedForm, authAdmin).then((res: ErrorResponse) => { + postPewPew(parsedForm, authAdmin).then(async (res: ErrorResponse) => { log("postPewPew res", LogLevel.DEBUG, res); expect(res.status, JSON.stringify(res.json)).to.equal(200); const body: TestManagerError = res.json; @@ -123,6 +124,21 @@ describe("PewPew Util Integration", () => { sharedPewPewVersions.push(version); } log("sharedPewPewVersions: " + sharedPewPewVersions, LogLevel.DEBUG); + try { + const pewpewFiles = await PpaasS3File.getAllFilesInS3({ + s3Folder: `${PEWPEW_BINARY_FOLDER}/${version}`, + localDirectory: process.env.TEMP || "/tmp" + }); + expect(pewpewFiles).to.not.equal(undefined); + expect(pewpewFiles.length).to.be.greaterThan(0); + const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + expect(pewpewFile).to.not.equal(undefined); + expect(pewpewFile?.tags).to.not.equal(undefined); + expect(pewpewFile?.tags?.get(VERSION_TAG_NAME)).to.equal(version); + } catch (error) { + log("postPewPew error while checking latest tag: ", LogLevel.ERROR, error); + throw error; + } done(); }).catch((error) => { log("postPewPew error", LogLevel.ERROR, error); @@ -138,7 +154,7 @@ describe("PewPew Util Integration", () => { files: filesLegacyPewpew }; log("postPewPew parsedForm", LogLevel.DEBUG, parsedForm); - postPewPew(parsedForm, authAdmin).then((res: ErrorResponse) => { + postPewPew(parsedForm, authAdmin).then(async (res: ErrorResponse) => { log("postPewPew res", LogLevel.DEBUG, res); expect(res.status, JSON.stringify(res.json)).to.equal(200); const body: TestManagerError = res.json; @@ -147,14 +163,34 @@ describe("PewPew Util Integration", () => { expect(body.message).to.not.equal(undefined); expect(body.message).to.include("PewPew uploaded, version"); expect(body.message).to.include("as latest"); - const version = latestPewPewVersion; + const match: RegExpMatchArray | null = body.message.match(/PewPew uploaded, version: (\d+\.\d+\.\d+)/); + log(`pewpew match: ${match}`, LogLevel.DEBUG, match); + expect(match, "pewpew match").to.not.equal(null); + expect(match!.length, "pewpew match.length").to.be.greaterThan(1); + const version: string = match![1]; // If this runs before the other acceptance tests populate the shared pewpew versions if (!sharedPewPewVersions) { - sharedPewPewVersions = [version]; - } else if (!sharedPewPewVersions.includes(version)) { - sharedPewPewVersions.push(version); + sharedPewPewVersions = [latestPewPewVersion]; + } else if (!sharedPewPewVersions.includes(latestPewPewVersion)) { + sharedPewPewVersions.push(latestPewPewVersion); } log("sharedPewPewVersions: " + sharedPewPewVersions, LogLevel.DEBUG); + try { + const pewpewFiles = await PpaasS3File.getAllFilesInS3({ + s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}`, + localDirectory: process.env.TEMP || "/tmp" + }); + expect(pewpewFiles).to.not.equal(undefined); + expect(pewpewFiles.length).to.be.greaterThan(0); + const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + expect(pewpewFile).to.not.equal(undefined); + expect(pewpewFile?.tags).to.not.equal(undefined); + expect(pewpewFile?.tags?.get(VERSION_TAG_NAME)).to.equal(version); + } catch (error) { + log("postPewPew error while checking latest tag: ", LogLevel.ERROR, error); + throw error; + } + currentPewPewLatestVersion = version; done(); }).catch((error) => { log("postPewPew error", LogLevel.ERROR, error); @@ -198,6 +234,20 @@ describe("PewPew Util Integration", () => { }); describe("getPewpew", () => { + before(async () => { + try { + const pewpewFiles = await PpaasS3File.getAllFilesInS3({ + s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}`, + localDirectory: process.env.TEMP || "/tmp" + }); + expect(pewpewFiles).to.not.equal(undefined); + const pewpewFile = pewpewFiles.find( (file) => file.filename === "pewpew"); + expect(pewpewFile).to.not.equal(undefined); + } catch (error) { + log("getCurrentPewPewLatest Version failed: ", LogLevel.ERROR, error); + throw error; + } + }); it("getPewpew should respond 200 OK", (done: Mocha.Done) => { expect(sharedPewPewVersions, "sharedPewPewVersions").to.not.equal(undefined); getPewpew().then((res: ErrorResponse | PewPewVersionsResponse) => { @@ -213,6 +263,12 @@ describe("PewPew Util Integration", () => { done(); }).catch((error) => done(error)); }); + + it("getCurrentPewPewLatestVersion", async () => { + expect(currentPewPewLatestVersion, "currentPewPewLatestVersion").to.not.equal(undefined); + const result = await getCurrentPewPewLatestVersion(); + expect(result, "result").to.equal(currentPewPewLatestVersion); + }); }); describe("deletePewPew", () => { diff --git a/controller/pages/admin.tsx b/controller/pages/admin.tsx index 4b73e9d0..5e6c86b5 100644 --- a/controller/pages/admin.tsx +++ b/controller/pages/admin.tsx @@ -329,7 +329,7 @@ const Admin = ({ authPermission, versionInitalProps, error: propsError }: AdminP export const getServerSideProps: GetServerSideProps = async (ctx: GetServerSidePropsContext): Promise> => { - let versionInitalProps: VersionInitalProps = { pewpewVersion: "", loading: false, pewpewVersions: [], error: true }; + let versionInitalProps: VersionInitalProps = { pewpewVersion: "", loading: false, pewpewVersions: [], latestPewPewVersion: "", error: true }; try { // Authenticate const authPermissions: AuthPermissions | string = await authPage(ctx, AuthPermission.Admin); diff --git a/controller/pages/api/util/pewpew.ts b/controller/pages/api/util/pewpew.ts index 35e8e5be..52b73db4 100644 --- a/controller/pages/api/util/pewpew.ts +++ b/controller/pages/api/util/pewpew.ts @@ -33,6 +33,7 @@ logger.config.LogFileName = "ppaas-controller"; const deleteS3 = s3.deleteObject; export const PEWPEW_EXECUTABLE_NAME: string = "pewpew"; const PEWPEW_EXECUTABLE_NAME_WINDOWS: string = "pewpew.exe"; +export const VERSION_TAG_NAME: string = "version"; /** * Queries S3 for all objects in the pewpew/ folder that end with pewpew (not pewpew.exe). @@ -74,6 +75,16 @@ export async function getPewpew (): Promise[] = []; const versionLogDisplay = latest ? `${version} as latest` : version; const versionFolder = latest ? latestPewPewVersion : version; log(PEWPEW_EXECUTABLE_NAME + " only upload, version: " + versionLogDisplay, LogLevel.DEBUG, files); // Pass in an override Map to override the default tags and not set a "test" tag - const tags = new Map([["pewpew", "true"]]); + const tags = new Map([[PEWPEW_BINARY_FOLDER, "true"], [VERSION_TAG_NAME, version]]); if (Array.isArray(files.additionalFiles)) { for (const file of files.additionalFiles) { uploadPromises.push(uploadFile(file, `${PEWPEW_BINARY_FOLDER}/${versionFolder}`, tags)); @@ -149,6 +159,16 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP uploadPromises.push(uploadFile(files.additionalFiles, `${PEWPEW_BINARY_FOLDER}/${versionFolder}`, tags)); } await Promise.all(uploadPromises); + // If latest version is being updated: + if (latest) { + global.currentLatestVersion = version; + try { + + log("Sucessfully saved PewPew's latest version to file. ", LogLevel.INFO); + } catch (error) { + log("Writing latest PewPew's latest version to file failed: ", LogLevel.ERROR, error); + } + } log(PEWPEW_EXECUTABLE_NAME + " only uploaded, version: " + versionLogDisplay, LogLevel.INFO, { files, authPermissions: getLogAuthPermissions(authPermissions) }); return { json: { message: "PewPew uploaded, version: " + versionLogDisplay }, status: 200 }; } else { @@ -215,3 +235,17 @@ export async function deletePewPew (query: Partial { + if (global.currentLatestVersion) { + return global.currentLatestVersion; + } + try { + const pewpewTags = await s3.getTags({s3Folder: `${PEWPEW_BINARY_FOLDER}/${latestPewPewVersion}`, filename: PEWPEW_EXECUTABLE_NAME}); + global.currentLatestVersion = pewpewTags && pewpewTags.get(VERSION_TAG_NAME); // <- change to get the tag here + return global.currentLatestVersion; + } catch (error) { + log("Could not load latest pewpew in file", LogLevel.ERROR, error); + throw error; + } +} diff --git a/controller/test/pewpew.spec.ts b/controller/test/pewpew.spec.ts index fdeefac0..97cd249a 100644 --- a/controller/test/pewpew.spec.ts +++ b/controller/test/pewpew.spec.ts @@ -15,7 +15,9 @@ import { } from "../pages/api/util/util"; import { PEWPEW_EXECUTABLE_NAME, + VERSION_TAG_NAME, deletePewPew, + getCurrentPewPewLatestVersion, getPewPewVersionsInS3, getPewpew, postPewPew @@ -129,6 +131,18 @@ describe("PewPew Util", () => { resetMockS3(); }); + describe("getCurrentPewPewLatestVersion", () => { + it("getCurrentPewPewLatestVersion should return version with latest tag from S3", (done: Mocha.Done) => { + const expected = "0.5.13"; + mockGetObjectTagging(new Map([[VERSION_TAG_NAME, expected]])); + getCurrentPewPewLatestVersion().then((result: string | undefined) => { + log("getPewPewVersionsInS3()", LogLevel.DEBUG, result); + expect(result).to.equal(expected); + done(); + }).catch((error) => done(error)); + }); + }); + describe("getPewPewVersionsInS3", () => { it("getPewPewVersionsInS3() should return array with elements", (done: Mocha.Done) => { mockListObjects(versions.map((version): S3Object => ({ ...s3Object, Key: `${pewpewS3Folder}/${version}/${pewpewFilename}` }))); diff --git a/guide/results-viewer-react/.eslintrc b/guide/results-viewer-react/.eslintrc index f5a20c45..b35016bd 100644 --- a/guide/results-viewer-react/.eslintrc +++ b/guide/results-viewer-react/.eslintrc @@ -50,6 +50,7 @@ "space-infix-ops":"warn", "space-before-blocks": "warn", "keyword-spacing": "warn", + "key-spacing": 1, "strict": 1, "comma-dangle": 1, "triple-equals": 0, diff --git a/guide/results-viewer-react/src/components/Modal/story.tsx b/guide/results-viewer-react/src/components/Modal/story.tsx index 3ad4147a..fccf8c7b 100644 --- a/guide/results-viewer-react/src/components/Modal/story.tsx +++ b/guide/results-viewer-react/src/components/Modal/story.tsx @@ -63,7 +63,7 @@ const ModalUploadHarDemo = () => {
{files.map((file, index) => (
- + {file.name}
))} diff --git a/guide/results-viewer-react/src/components/YamlUrls/story.tsx b/guide/results-viewer-react/src/components/YamlUrls/story.tsx index 226e6cc0..7f6dfc5a 100644 --- a/guide/results-viewer-react/src/components/YamlUrls/story.tsx +++ b/guide/results-viewer-react/src/components/YamlUrls/story.tsx @@ -45,9 +45,9 @@ const propsLoaded: UrlProps = { url: "https://www.pewpew.org", hitRate: "1hpm", headers: [ - {id: "0", name : "Default Header", value: "Here is where default headers are. These next 3 are the only default. Authorization header only shows up when Authenticated button is selected"}, + {id: "0", name: "Default Header", value: "Here is where default headers are. These next 3 are the only default. Authorization header only shows up when Authenticated button is selected"}, ...getDefaultHeaders(true), - {id: "1", name : "Har Header", value: "Here is where har headers would be"} + {id: "1", name: "Har Header", value: "Here is where har headers would be"} ], method: "GET", authorization: null },