From a8bee2dfdbe50bc37ef7bd9de4863bbfcc87414e Mon Sep 17 00:00:00 2001
From: Bryan <154275531+bryan-e-lopez@users.noreply.github.com>
Date: Mon, 18 Mar 2024 11:55:47 -0400
Subject: [PATCH] Adding toaster, dynamically checking pewpewVersion (#201)
* adding files to dynamically show latest
pewpew version on Toaster
* cleaning up code for lintter to pass
* refactoring code to use tags in S3
* removing files from tsconfig
* fixing lint rules
* fixing spacing as local linter didnt catch those but the build did
* missed one space
* adding tests, cleaning up files
* removing trailing spaces failing on build
* adding DS_Store - make specific file to gitignore
* updating storybook to show pewpew latest version output
---
.gitignore | 2 +-
.../components/PewPewVersions/index.tsx | 5 ++
.../components/PewPewVersions/initialProps.ts | 5 +-
.../components/PewPewVersions/story.tsx | 5 +-
controller/components/StartTestForm/story.tsx | 2 +
controller/components/Toaster/index.tsx | 49 ++++++++++++
controller/components/Toaster/story.tsx | 16 ++++
controller/integration/pewpew.spec.ts | 74 ++++++++++++++++---
controller/pages/admin.tsx | 2 +-
controller/pages/api/util/pewpew.ts | 38 +++++++++-
controller/test/pewpew.spec.ts | 14 ++++
11 files changed, 196 insertions(+), 16 deletions(-)
create mode 100644 controller/components/Toaster/index.tsx
create mode 100644 controller/components/Toaster/story.tsx
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/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/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/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 75b85d64..9fb2e6ba 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";
@@ -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 = {};
@@ -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;
@@ -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);
@@ -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;
@@ -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);
@@ -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) => {
@@ -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", () => {
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 38a1d8a2..6b33b52f 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
@@ -106,6 +108,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}` })));