Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding toaster, dynamically checking pewpewVersion #201

Merged
merged 12 commits into from
Mar 18, 2024
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ testmerge.json
.env.development.local*
.env.test.local*
.env.production.local*

.DS_Store
5 changes: 5 additions & 0 deletions controller/components/PewPewVersions/index.tsx
Original file line number Diff line number Diff line change
@@ -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)`
Expand All @@ -24,6 +25,7 @@ const VersionDivSelect = styled(Div)`
export interface VersionInitalProps {
pewpewVersion: string;
pewpewVersions: string[];
latestPewPewVersion: string;
loading: boolean;
error: boolean;
}
Expand All @@ -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) => (<option value={version} key={version}>{version}</option>));
}
Expand All @@ -53,6 +57,7 @@ export const PewPewVersions = ({
{loading && <VersionDivSelect>Loading...</VersionDivSelect>}
{!loading && !error && <VersionDivSelect><select name={name} value={pewpewVersion} onChange={onChange}>{optionItems} </select></VersionDivSelect>}
{error && <VersionDivSelect><Danger>Could not load the current PewPew Versions</Danger></VersionDivSelect>}
<Toaster id="toaster" message={toasterMessage}/>
</VersionDiv>
);
};
Expand Down
5 changes: 4 additions & 1 deletion controller/components/PewPewVersions/initialProps.ts
Original file line number Diff line number Diff line change
@@ -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<VersionInitalProps> => {
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);
Expand All @@ -16,6 +17,7 @@ export const getServerSideProps = async (): Promise<VersionInitalProps> => {
return {
pewpewVersion: latestPewPewVersion, // We always want to default to latest
pewpewVersions,
latestPewPewVersion: currentPewPewLatestVersion || "unknown",
loading: false,
error: false
};
Expand All @@ -25,6 +27,7 @@ export const getServerSideProps = async (): Promise<VersionInitalProps> => {
return {
pewpewVersion: "",
pewpewVersions: [],
latestPewPewVersion: "unknown",
loading: false,
error: true
};
Expand Down
5 changes: 3 additions & 2 deletions controller/components/PewPewVersions/story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions controller/components/StartTestForm/story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const versionInitalPropsEmpty: VersionInitalProps = {
pewpewVersion: "",
loading: false,
pewpewVersions: [],
latestPewPewVersion: "unknown",
error: true
};
const queueInitialProps: QueueInitialProps = {
Expand All @@ -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;
Expand Down
49 changes: 49 additions & 0 deletions controller/components/Toaster/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ToasterProps> = ({ 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 <Container id={id}>{message}</Container>;
};

export default Toaster;
16 changes: 16 additions & 0 deletions controller/components/Toaster/story.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Toaster>;

export const Default: StoryFn = () => (
<React.Fragment>
<GlobalStyle />
<Toaster id="toaster" message={"Type your message here: "}/>
</React.Fragment>
);
74 changes: 65 additions & 9 deletions controller/integration/pewpew.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -28,6 +28,7 @@ const pewpewZipFile: File = createFormidableFile(
);
let sharedPewPewVersions: string[] | undefined;
let uploadedPewPewVersion: string | undefined; // Used for delete
let currentPewPewLatestVersion: string | undefined;

describe("PewPew Util Integration", () => {
let files: Files = {};
Expand Down Expand Up @@ -75,7 +76,7 @@ describe("PewPew Util Integration", () => {
files
};
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;
Expand All @@ -97,6 +98,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);
Expand All @@ -112,7 +128,7 @@ describe("PewPew Util Integration", () => {
files
};
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;
Expand All @@ -121,14 +137,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);
Expand All @@ -138,6 +174,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) => {
Expand All @@ -153,6 +203,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", () => {
Expand Down
2 changes: 1 addition & 1 deletion controller/pages/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ const Admin = ({ authPermission, versionInitalProps, error: propsError }: AdminP

export const getServerSideProps: GetServerSideProps =
async (ctx: GetServerSidePropsContext): Promise<GetServerSidePropsResult<AdminProps>> => {
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);
Expand Down
38 changes: 36 additions & 2 deletions controller/pages/api/util/pewpew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -74,6 +75,16 @@ export async function getPewpew (): Promise<ErrorResponse | PewPewVersionsRespon
}
}

// Store these as maps for fast lookup oldest can be found by lastRequested, lastUpdated, and lastChecked
// We can't use static variables since the memory spaces aren't shared between api and getServerSideProps
// https://stackoverflow.com/questions/70260701/how-to-share-data-between-api-route-and-getserversideprops
declare global {
// https://stackoverflow.com/questions/68481686/type-typeof-globalthis-has-no-index-signature
/** The pewpew version that 'latest' is currently set to */
// eslint-disable-next-line no-var
var currentLatestVersion: string | undefined;
}

/**
* defines the POST /api/pewpew route which posts new versions of pewpew to s3
* @param parsedForm ParsedForm with the data
Expand Down Expand Up @@ -134,13 +145,12 @@ export async function postPewPew (parsedForm: ParsedForm, authPermissions: AuthP
if (version === null) {
return { json: { message: `${match[1]} is not a valid semver version: ${version}` }, status: 400 };
}

const uploadPromises: Promise<PpaasS3File>[] = [];
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<string, string>([["pewpew", "true"]]);
const tags = new Map<string, string>([[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));
Expand All @@ -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 {
Expand Down Expand Up @@ -215,3 +235,17 @@ export async function deletePewPew (query: Partial<Record<string, string | strin
throw error;
}
}

export async function getCurrentPewPewLatestVersion (): Promise<string | undefined> {
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;
}
}
Loading