Merge to main (#17)
* Fix sign up form bug * Add run.sh to run setup and run web app * Output node version * Update readme with run.sh * Fix file upload form to handle ota_update service * Enable file upload form Enable error boundary to catch React errors (#7) Fix warning for link noreferrer Include authorization header with file upload * Remove default localhost settings (#8) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix json link format and remove localhost default settings (#10) * Remove default localhost settings Replace with deployment settings * Fix for upload data format * Fix test data for last commit * Fix link data format * Fix link json again (#12) Use id token instead of access token * nginx things * Web Worker Sign Out and Use Go API (#13) * Calculate checksum and send with file upload * Limit file upload and display rejected file error * Add sign in timeout * Check auth token structure before setting Clean up * Use web worker timer to sign out Remove checksum Point to Go ota update * Remove checksum dependency * Use compute auth service and fix static code analyzer warnings (#15) * Clean up formatting * Use new compute_auth service Implment SSO Implement token refresh Clean up unit tests * Fix unit tests * Fix auth test Fix warnings * Update default settings for compute_auth * Change main UI layout and add VINs to add and upload forms (#16) * Add new upload update package form Add new add vehicle form Add new side menu layout Add new toolbar layout Update and add unit tests * Enable add get and add vehicles * Integration issues with ota_update service * Update get vehicle JSON format * Fix related unit test Add release notes field * Add StatusContext to display error and status messages * Handle api error json (#18) * Handle api error json * Fix get vehicles error handling Update .env.template Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
REACT_APP_AUTH_SERVICE_URL = https://dev-auth.fiskerdps.com
|
REACT_APP_AUTH_SERVICE_URL = https://dev-auth.fiskerdps.com
|
||||||
REACT_APP_UPLOAD_SERVICE_URL = http://localhost:8080/api/upload
|
REACT_APP_UPLOAD_SERVICE_URL = https://gw-dev.fiskerdps.com
|
||||||
|
REACT_APP_AUTH_CALLBACK_URL = https://dev-ota-admin.fiskerdps.com/
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ FROM node:12-alpine as builder
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY .env.template .env
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
COPY --from=builder build /usr/share/nginx/html
|
COPY --from=builder build /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
@@ -4,6 +4,8 @@ Front-end web application for administarting OTA services
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
|
Run `./run.sh` from the terminal or
|
||||||
|
|
||||||
1. Install Node 12
|
1. Install Node 12
|
||||||
2. Run `npm install`
|
2. Run `npm install`
|
||||||
3. Setup environment variables listed in .env.template
|
3. Setup environment variables listed in .env.template
|
||||||
|
|||||||
16
nginx.conf
16
nginx.conf
@@ -1,13 +1,9 @@
|
|||||||
events { worker_connections 1024; }
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
http {
|
location / {
|
||||||
server {
|
try_files $uri /index.html;
|
||||||
listen 80;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri /index.html;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3601
package-lock.json
generated
3601
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
|||||||
"@testing-library/react": "^11.2.2",
|
"@testing-library/react": "^11.2.2",
|
||||||
"@testing-library/user-event": "^12.6.0",
|
"@testing-library/user-event": "^12.6.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
|||||||
6
run.sh
Executable file
6
run.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
node -v
|
||||||
|
npm install
|
||||||
|
cp .env.template .env
|
||||||
|
npm start
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Typography } from "@material-ui/core";
|
import { Typography } from "@material-ui/core";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import useStyles from '../Styles';
|
import useStyles from "../useStyles";
|
||||||
|
|
||||||
const PageNotFound = () => {
|
const PageNotFound = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
@@ -12,6 +12,6 @@ const PageNotFound = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PageNotFound;
|
export default PageNotFound;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
jest.mock("../Contexts/UserContext");
|
jest.mock("../Contexts/UserContext");
|
||||||
jest.mock("../Contexts/FileUploadContext");
|
jest.mock("../Contexts/FileUploadContext");
|
||||||
|
jest.mock("../Contexts/VehicleContext");
|
||||||
|
|
||||||
import { render, screen, cleanup, waitForElementToBeRemoved, waitFor } from "@testing-library/react"
|
import { render, screen, cleanup, waitForElementToBeRemoved } from "@testing-library/react";
|
||||||
import { setToken } from "../Contexts/UserContext";
|
import { setToken } from "../Contexts/UserContext";
|
||||||
import App from ".";
|
import App from ".";
|
||||||
|
|
||||||
const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }};
|
const TEST_TOKEN = { idToken: { jwtToken: "TEST" } };
|
||||||
const LOADING_STATUS = "Loading...";
|
const LOADING_STATUS = "Loading...";
|
||||||
|
|
||||||
const renderRoute = async (route) => {
|
const renderRoute = async (route) => {
|
||||||
@@ -26,40 +27,40 @@ describe("App", () => {
|
|||||||
|
|
||||||
it("Route / unauthenticated", async () => {
|
it("Route / unauthenticated", async () => {
|
||||||
const container = await renderRoute("/");
|
const container = await renderRoute("/");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Sign in");
|
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Route /signup unauthenticated", async () => {
|
|
||||||
const container = await renderRoute("/signup");
|
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Sign up");
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /home unauthenticated", async () => {
|
it("Route /home unauthenticated", async () => {
|
||||||
const container = await renderRoute("/home");
|
const container = await renderRoute("/home");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Sign in");
|
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Route /vehicle-add unauthenticated", async () => {
|
||||||
|
const container = await renderRoute("/vehicle-add");
|
||||||
|
expect(container.querySelector("span.MuiButton-label").innerHTML).toEqual("Sign In");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route / authenticated", async () => {
|
it("Route / authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_TOKEN);
|
||||||
const container = await renderRoute("/");
|
const container = await renderRoute("/");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
|
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Route /signup authenticated", async () => {
|
|
||||||
setToken(TEST_TOKEN);
|
|
||||||
const container = await renderRoute("/signup");
|
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Route /home authenticated", async () => {
|
it("Route /home authenticated", async () => {
|
||||||
setToken(TEST_TOKEN);
|
setToken(TEST_TOKEN);
|
||||||
const container = await renderRoute("/home");
|
const container = await renderRoute("/home");
|
||||||
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
|
expect(container.querySelector("h1").innerHTML).toEqual("Upload Update Package");
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Route /vehicle-add authenticated", async () => {
|
||||||
|
setToken(TEST_TOKEN);
|
||||||
|
const container = await renderRoute("/vehicle-add");
|
||||||
|
expect(container.querySelector("h1").innerHTML).toEqual("Add Vehicle");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,5 +76,4 @@ describe("App", () => {
|
|||||||
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
|
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,23 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { UserProvider } from '../Contexts/UserContext';
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import SiteRoutes from '../Routes/SiteRoutes';
|
import { UserProvider } from "../Contexts/UserContext";
|
||||||
|
import { StatusProvider } from "../Contexts/StatusContext";
|
||||||
|
import { CssBaseline } from "@material-ui/core";
|
||||||
|
import MenuDrawer from "../Layouts/MenuDrawer";
|
||||||
|
import SiteRoutes from "../Routes/SiteRoutes";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<UserProvider>
|
<StatusProvider>
|
||||||
<SiteRoutes />
|
<UserProvider>
|
||||||
</UserProvider>
|
<CssBaseline />
|
||||||
|
<BrowserRouter>
|
||||||
|
<MenuDrawer>
|
||||||
|
<SiteRoutes />
|
||||||
|
</MenuDrawer>
|
||||||
|
</BrowserRouter>
|
||||||
|
</UserProvider>
|
||||||
|
</StatusProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [cancelUpload, setCancelUpload] = useState(null);
|
const [cancelUpload, setCancelUpload] = useState(null);
|
||||||
|
const [linkURL, setLinkURL] = useState(null);
|
||||||
|
const [files, setFiles] = useState(null);
|
||||||
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
setCancelUpload(null);
|
setCancelUpload(null);
|
||||||
@@ -23,37 +25,80 @@ export const FileUploadProvider = ({ children }) => {
|
|||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = async (files) => {
|
const validateUpload = (formData, accessToken, uploadFiles) => {
|
||||||
try {
|
if (!formData) {
|
||||||
if (!files || files.length === 0) throw new Error("No file provided");
|
throw new Error("Missing package update data");
|
||||||
|
}
|
||||||
|
|
||||||
const file = files[0].file;
|
if (!formData.packagename || formData.packagename.length === 0) {
|
||||||
|
throw new Error("Package name required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.version || formData.version.length === 0) {
|
||||||
|
throw new Error("Package update version required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.vehicles || formData.vehicles.length === 0) {
|
||||||
|
throw new Error("Vehicles required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uploadFiles || uploadFiles.length === 0) {
|
||||||
|
throw new Error("File required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken || accessToken.length === 0) {
|
||||||
|
throw new Error("Access token required");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = async (formData, accessToken, uploadFiles) => {
|
||||||
|
validateUpload(formData, accessToken, uploadFiles);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = uploadFiles[0];
|
||||||
const filename = file.name;
|
const filename = file.name;
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
setLinkURL(null);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setStatus(`Uploading ${filename}`);
|
setStatus(`Uploading ${filename}`);
|
||||||
setCancelUpload(getCancelToken());
|
setCancelUpload(getCancelToken());
|
||||||
|
|
||||||
const result = await uploadFile(file, setProgress, cancelUpload);
|
const { data } = await uploadFile(
|
||||||
const url = ((result && result.url) ? result.url : "No URL available");
|
file,
|
||||||
setStatus(`Uploaded ${filename}\n${url}`);
|
formData,
|
||||||
|
accessToken,
|
||||||
|
setProgress,
|
||||||
|
cancelUpload
|
||||||
|
);
|
||||||
|
if (data.message) {
|
||||||
|
throw new Error(`${data.error}. ${data.message}`);
|
||||||
|
}
|
||||||
|
const url = data && data.link ? data.link : "No URL available";
|
||||||
|
setLinkURL(url);
|
||||||
|
setStatus(`Uploaded ${filename}`);
|
||||||
setCancelUpload(null);
|
setCancelUpload(null);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
setUploading(true);
|
||||||
setStatus(`Error occured: ${e.message}`);
|
setStatus(`Error occured: ${e.message}`);
|
||||||
|
setProgress(-1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileUploadContext.Provider value={{
|
<FileUploadContext.Provider
|
||||||
uploading,
|
value={{
|
||||||
progress,
|
uploading,
|
||||||
status,
|
progress,
|
||||||
upload,
|
status,
|
||||||
cancel,
|
linkURL,
|
||||||
}}>
|
files,
|
||||||
|
upload,
|
||||||
|
cancel,
|
||||||
|
setFiles,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</FileUploadContext.Provider>
|
</FileUploadContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,96 @@
|
|||||||
jest.mock("../../services/uploadFile");
|
jest.mock("../../services/uploadFile");
|
||||||
|
|
||||||
import {uploadFile, getCancelToken, setUploadFileResponse, setUploadFileDelay, getIssuedCancelToken } from "../../services/uploadFile"
|
import {
|
||||||
import { FileUploadProvider, useFileUploadContext } from "../Contexts/FileUploadContext";
|
render,
|
||||||
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
|
cleanup,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
|
||||||
|
import { setUploadFileDelay } from "../../services/uploadFile";
|
||||||
|
import {
|
||||||
|
FileUploadProvider,
|
||||||
|
useFileUploadContext,
|
||||||
|
} from "../Contexts/FileUploadContext";
|
||||||
|
import { StatusProvider, useStatusContext } from "../Contexts/StatusContext";
|
||||||
|
|
||||||
|
const checkState = (uploading, progress, status, linkURL, message) => {
|
||||||
|
expect(screen.getByTestId("uploading").innerHTML).toEqual(uploading);
|
||||||
|
expect(screen.getByTestId("progress").innerHTML).toEqual(progress);
|
||||||
|
expect(screen.getByTestId("status").innerHTML).toEqual(status);
|
||||||
|
expect(screen.getByTestId("linkURL").innerHTML).toEqual(linkURL);
|
||||||
|
expect(screen.getByTestId("message").innerHTML).toEqual(message);
|
||||||
|
};
|
||||||
|
|
||||||
describe("FileUploadContext", () => {
|
describe("FileUploadContext", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const TestComp = () => {
|
const TestComp = () => {
|
||||||
const { progress, uploading, status, upload, cancel } = useFileUploadContext();
|
const {
|
||||||
|
progress,
|
||||||
|
uploading,
|
||||||
|
status,
|
||||||
|
linkURL,
|
||||||
|
upload,
|
||||||
|
cancel,
|
||||||
|
setFiles,
|
||||||
|
} = useFileUploadContext();
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const TEST_FILE = [{ name: "test.jpg", size: 0 }];
|
||||||
|
const TEST_ACCESSTOKEN = "ACCESSTOKEN";
|
||||||
|
const TEST_FORMDATA = {
|
||||||
|
packagename: "TEST",
|
||||||
|
version: "VERSION",
|
||||||
|
vehicles: ["VIN"],
|
||||||
|
};
|
||||||
|
const exec = async (form, token, file) => {
|
||||||
|
try {
|
||||||
|
await upload(form, token, file);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-testid="uploading">{uploading.toString()}</div>
|
<div data-testid="uploading">{uploading.toString()}</div>
|
||||||
<div data-testid="progress">{progress.toString()}</div>
|
<div data-testid="progress">{progress.toString()}</div>
|
||||||
<div data-testid="status">{status}</div>
|
<div data-testid="status">{status}</div>
|
||||||
<button data-testid="uploadNoFile" onClick={() => upload()}/>
|
<div data-testid="message">{message}</div>
|
||||||
<button data-testid="upload" onClick={() => upload([{ file: { name: "test.jpg" }}])}/>
|
<div data-testid="linkURL">{linkURL}</div>
|
||||||
<button data-testid="cancel" onClick={() => cancel()}/>
|
<button
|
||||||
|
data-testid="uploadNoFile"
|
||||||
|
onClick={() => {
|
||||||
|
exec(TEST_FORMDATA, TEST_ACCESSTOKEN, null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="uploadNoToken"
|
||||||
|
onClick={() => {
|
||||||
|
exec(TEST_FORMDATA, null, TEST_FILE);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="uploadNoFormData"
|
||||||
|
onClick={() => {
|
||||||
|
exec({}, TEST_ACCESSTOKEN, TEST_FILE);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="upload"
|
||||||
|
onClick={() => exec(TEST_FORMDATA, TEST_ACCESSTOKEN, TEST_FILE)}
|
||||||
|
/>
|
||||||
|
<button data-testid="cancel" onClick={() => cancel()} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
render(<FileUploadProvider><TestComp /></FileUploadProvider>);
|
render(
|
||||||
|
<StatusProvider>
|
||||||
|
<FileUploadProvider>
|
||||||
|
<TestComp />
|
||||||
|
</FileUploadProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -28,34 +98,53 @@ describe("FileUploadContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Initial state", async () => {
|
it("Initial state", async () => {
|
||||||
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
|
checkState("false", "0", "", "", "");
|
||||||
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
|
});
|
||||||
expect(screen.getByTestId("status").innerHTML).toEqual("");
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Upload no file", async () => {
|
it("Upload no file", async () => {
|
||||||
fireEvent.click(screen.getByTestId("uploadNoFile"));
|
fireEvent.click(screen.getByTestId("uploadNoFile"));
|
||||||
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
|
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||||
expect(screen.getByTestId("status").innerHTML).toEqual("Error occured: No file provided");
|
);
|
||||||
})
|
checkState("false", "0", "", "", "File required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Upload no access token", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("uploadNoToken"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||||
|
);
|
||||||
|
checkState("false", "0", "", "", "Access token required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Upload no form data", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("uploadNoFormData"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("message").innerHTML).not.toBe("")
|
||||||
|
);
|
||||||
|
checkState("false", "0", "", "", "Package name required");
|
||||||
|
});
|
||||||
|
|
||||||
it("Upload file", async () => {
|
it("Upload file", async () => {
|
||||||
fireEvent.click(screen.getByTestId("upload"));
|
fireEvent.click(screen.getByTestId("upload"));
|
||||||
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("100"));
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
|
expect(screen.getByTestId("progress").innerHTML).toEqual("100")
|
||||||
expect(screen.getByTestId("status").innerHTML).toEqual("Uploaded test.jpg\nCLOUDFRONT_URL");
|
);
|
||||||
})
|
checkState("true", "100", "Uploaded test.jpg", "CLOUDFRONT_URL", "");
|
||||||
|
});
|
||||||
|
|
||||||
it("Cancel upload", async () => {
|
it("Cancel upload", async () => {
|
||||||
setUploadFileDelay(true);
|
setUploadFileDelay(true);
|
||||||
fireEvent.click(screen.getByTestId("upload"));
|
fireEvent.click(screen.getByTestId("upload"));
|
||||||
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("50"));
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
|
expect(screen.getByTestId("progress").innerHTML).toEqual("50")
|
||||||
expect(screen.getByTestId("status").innerHTML).toEqual("Uploading test.jpg");
|
);
|
||||||
|
checkState("true", "50", "Uploading test.jpg", "", "");
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("cancel"));
|
fireEvent.click(screen.getByTestId("cancel"));
|
||||||
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("0"));
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
|
expect(screen.getByTestId("progress").innerHTML).toEqual("0")
|
||||||
expect(screen.getByTestId("status").innerHTML).toEqual("Upload cancelled");
|
);
|
||||||
})
|
checkState("false", "0", "Upload cancelled", "", "");
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|||||||
20
src/components/Contexts/StatusContext.jsx
Normal file
20
src/components/Contexts/StatusContext.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
|
const StatusContext = React.createContext();
|
||||||
|
|
||||||
|
export const StatusProvider = ({ children }) => {
|
||||||
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusContext.Provider
|
||||||
|
value={{
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StatusContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStatusContext = () => useContext(StatusContext);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import auth from '../../services/auth';
|
import auth from "../../services/auth";
|
||||||
|
import getTimerWorker from "../../services/timer";
|
||||||
|
|
||||||
const UserContext = React.createContext();
|
const UserContext = React.createContext();
|
||||||
|
|
||||||
@@ -7,87 +8,153 @@ export const UserProvider = ({ children }) => {
|
|||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [token, setToken] = useState(null);
|
const [token, setToken] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
let timer;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!localStorage) return;
|
if (!localStorage) return;
|
||||||
const token = JSON.parse(localStorage.getItem("token"));
|
const t = JSON.parse(localStorage.getItem("token"));
|
||||||
if (!token) return;
|
if (!t || !t.idToken || !t.idToken.jwtToken) return;
|
||||||
const { accessToken: { jwtToken }} = token;
|
if (!t.idToken.payload || !t.idToken.payload.exp) return;
|
||||||
const verifyToken = async (accessToken) => {
|
setToken(t);
|
||||||
const result = await auth.verify(accessToken);
|
|
||||||
if (result.authenticated) {
|
|
||||||
setToken(token);
|
|
||||||
} else {
|
|
||||||
await signOut();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
verifyToken(jwtToken);
|
|
||||||
return () => {};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signIn = async (username, password) => {
|
useEffect(() => {
|
||||||
|
if (!token) return;
|
||||||
|
verifyToken();
|
||||||
|
return () => {
|
||||||
|
if (timer) timer.terminate();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const refreshTokens = async () => {
|
||||||
|
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
|
||||||
|
const result = await refresh(token.refreshToken.token);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isError = (resp) => {
|
||||||
|
if (resp === null) return true;
|
||||||
|
if (resp && resp.error) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSessionTimer = () => {
|
||||||
|
const duration = 1000 * token.idToken.payload.exp - new Date().getTime();
|
||||||
|
if (!timer) {
|
||||||
|
timer = getTimerWorker();
|
||||||
|
timer.onMessage(async (e) => {
|
||||||
|
if (e.data === "timeout") {
|
||||||
|
const t = await refreshTokens();
|
||||||
|
if (!isError(t)) return;
|
||||||
|
signOut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
timer.start(duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = async () => {
|
||||||
try {
|
try {
|
||||||
if (!username) throw new Error('Email is required');
|
const {
|
||||||
if (!password) throw new Error('Password is required');
|
idToken: { jwtToken: idToken },
|
||||||
|
} = token;
|
||||||
|
const result = await auth.verify(idToken);
|
||||||
|
|
||||||
|
if (!result && !result.valid) {
|
||||||
|
const t = await refreshTokens();
|
||||||
|
if (!isError(t)) return;
|
||||||
|
signOut();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startSessionTimer();
|
||||||
|
} catch (e) {
|
||||||
|
signOut();
|
||||||
|
setError(`Verify error. ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const signIn = async (code) => {
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
setFetching(true);
|
setFetching(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const result = await auth.signIn(username, password);
|
result = await auth.signIn(code);
|
||||||
|
if (result.message) {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.message) throw new Error(result.message);
|
|
||||||
signedIn(result);
|
signedIn(result);
|
||||||
}
|
} catch (err) {
|
||||||
catch (error) {
|
setError(`Sign in error. ${err.message}`);
|
||||||
setError(error.message);
|
} finally {
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const signUp = async (username, password, confirmPassword) => {
|
const signOut = () => {
|
||||||
try {
|
setToken(null);
|
||||||
if (!username) throw new Error('Email is required');
|
if (localStorage) {
|
||||||
if (!password) throw new Error('Password is required');
|
localStorage.removeItem("token");
|
||||||
if (password !== confirmPassword) throw new Error('Passwords do not match');
|
}
|
||||||
|
return getLogoutURL();
|
||||||
|
};
|
||||||
|
|
||||||
|
const signedIn = (value) => {
|
||||||
|
setToken(value);
|
||||||
|
if (!localStorage || !value || !value.idToken) return;
|
||||||
|
localStorage.setItem("token", JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async (value) => {
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("Token required");
|
||||||
|
}
|
||||||
setFetching(true);
|
setFetching(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const result = await auth.signUp(username, password);
|
result = await auth.refresh(value);
|
||||||
if (result.message) throw new Error(result.message);
|
|
||||||
}
|
if (result.message) {
|
||||||
catch (error) {
|
throw new Error(result.message);
|
||||||
setError(error.message);
|
}
|
||||||
}
|
signedIn(result);
|
||||||
finally {
|
} catch (err) {
|
||||||
|
setError(`Refresh error. ${err.message}`);
|
||||||
|
} finally {
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const signOut = async () => {
|
const getAuthorizeURL = () => auth.ssoAuthorize();
|
||||||
setToken(null);
|
const getLogoutURL = () => auth.ssoLogout();
|
||||||
if (!localStorage) return;
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
};
|
|
||||||
|
|
||||||
const signedIn = (token) => {
|
|
||||||
setToken(token);
|
|
||||||
if (!localStorage || !token || !token.accessToken) return;
|
|
||||||
localStorage.setItem("token", JSON.stringify(token));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{
|
<UserContext.Provider
|
||||||
fetching,
|
value={{
|
||||||
token,
|
fetching,
|
||||||
error,
|
token,
|
||||||
setError,
|
error,
|
||||||
signIn,
|
setError,
|
||||||
signUp,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
}}>
|
refresh,
|
||||||
|
getAuthorizeURL,
|
||||||
|
getLogoutURL,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,90 +1,83 @@
|
|||||||
jest.mock("../../services/auth");
|
jest.mock("../../services/auth");
|
||||||
|
jest.mock("../../services/timer");
|
||||||
|
|
||||||
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
|
import {
|
||||||
import { UserProvider, useUserContext } from "../Contexts/UserContext";
|
render,
|
||||||
|
cleanup,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
import { UserProvider, useUserContext } from "../Contexts/UserContext";
|
||||||
import auth from "../../services/auth";
|
import auth from "../../services/auth";
|
||||||
|
import getTimerWorker from "../../services/timer";
|
||||||
|
|
||||||
const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }};
|
const TEST_TOKEN = {
|
||||||
|
idToken: {
|
||||||
|
jwtToken: "TEST",
|
||||||
|
payload: {
|
||||||
|
exp: new Date().getTime() / 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const INVALID_TOKEN_RESPONSE = {
|
||||||
|
error: "Bad Request Error",
|
||||||
|
message: "Bad Request Message",
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupRefreshEnv = (refreshResponse, valid) => {
|
||||||
|
auth.setRefreshResponse(refreshResponse);
|
||||||
|
auth.setVerifyResponse({ valid });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupSignInEnv = (refreshResponse, valid) => {
|
||||||
|
auth.setSignInResponse(refreshResponse);
|
||||||
|
auth.setVerifyResponse({ valid });
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkBaseResults = (error, fetching, token) => {
|
||||||
|
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||||
|
expect(screen.getByTestId("fetching").innerHTML).toEqual(fetching);
|
||||||
|
expect(screen.getByTestId("token").innerHTML).toEqual(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTokenResults = (timer, token) => {
|
||||||
|
expect(timer.start.mock.calls.length).toEqual(1);
|
||||||
|
expect(timer.onMessage.mock.calls.length).toEqual(1);
|
||||||
|
expect(timer.stop.mock.calls.length).toEqual(0);
|
||||||
|
expect(timer.terminate.mock.calls.length).toEqual(0);
|
||||||
|
if (!localStorage) {
|
||||||
|
expect(localStorage.getItem("token")).toEqual(token);
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe("UseContext", () => {
|
describe("UseContext", () => {
|
||||||
|
|
||||||
describe("Signup", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const TestComp = () => {
|
|
||||||
const { signUp, error, fetching } = useUserContext();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div data-testid="error">{error}</div>
|
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
|
||||||
<button data-testid="signUpNoEmail" onClick={() => signUp("")}/>
|
|
||||||
<button data-testid="signUpNoPassword" onClick={() => signUp("test@test.com", "")}/>
|
|
||||||
<button data-testid="signUpBadConfirm" onClick={() => signUp("test@test.com", "password", "")}/>
|
|
||||||
<button data-testid="signUp" onClick={() => signUp("test@test.com", "password", "password")}/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
render(<UserProvider><TestComp /></UserProvider>);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Initial state", () => {
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Error with no email address", () => {
|
|
||||||
fireEvent.click(screen.getByTestId("signUpNoEmail"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Error with no password", () => {
|
|
||||||
fireEvent.click(screen.getByTestId("signUpNoPassword"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Error with non-matching password", () => {
|
|
||||||
fireEvent.click(screen.getByTestId("signUpBadConfirm"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("Passwords do not match");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("No error sign up", async () => {
|
|
||||||
fireEvent.click(screen.getByTestId("signUp"));
|
|
||||||
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Handle server error", async () => {
|
|
||||||
auth.setSignUpResponse({ message: "SERVER-ERROR", error: "ERR" });
|
|
||||||
fireEvent.click(screen.getByTestId("signUp"));
|
|
||||||
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
|
|
||||||
auth.setSignUpResponse({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Signin", () => {
|
describe("Signin", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const TestComp = () => {
|
const TestComp = () => {
|
||||||
const { signIn, error, token, fetching } = useUserContext();
|
const { signIn, error, token, fetching } = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-testid="error">{error}</div>
|
<div data-testid="error">{error}</div>
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
<div data-testid="token">{JSON.stringify(token)}</div>
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
<button data-testid="signInNoEmail" onClick={() => signIn("")}/>
|
<button data-testid="signInNoCode" onClick={() => signIn("")} />
|
||||||
<button data-testid="signInNoPassword" onClick={() => signIn("test@test.com", "")}/>
|
<button
|
||||||
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
|
data-testid="signInInvalidCode"
|
||||||
|
onClick={() => signIn("INVALID_CODE")}
|
||||||
|
/>
|
||||||
|
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
render(<UserProvider><TestComp /></UserProvider>);
|
render(
|
||||||
|
<UserProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -92,43 +85,41 @@ describe("UseContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("Initial state", () => {
|
it("Initial state", () => {
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("");
|
checkBaseResults("", "false", "null");
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual("null");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Error with no email address", () => {
|
it("No auth code", () => {
|
||||||
fireEvent.click(screen.getByTestId("signInNoEmail"));
|
fireEvent.click(screen.getByTestId("signInNoCode"));
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
checkBaseResults("", "false", "null");
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual("null");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Error with no password", () => {
|
it("Invalid auth code", async () => {
|
||||||
fireEvent.click(screen.getByTestId("signInNoPassword"));
|
setupSignInEnv(INVALID_TOKEN_RESPONSE, false);
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
fireEvent.click(screen.getByTestId("signInInvalidCode"));
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual("null");
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
|
);
|
||||||
|
|
||||||
|
checkBaseResults("Sign in error. Bad Request Message", "false", "null");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("No error sign in", async () => {
|
it("Sign in form", async () => {
|
||||||
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
|
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
|
||||||
auth.setSignInResponse(TEST_TOKEN);
|
const timer = getTimerWorker();
|
||||||
fireEvent.click(screen.getByTestId("signIn"));
|
|
||||||
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
|
setupSignInEnv(TEST_TOKEN, true);
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("");
|
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual(TOKEN_STRING);
|
|
||||||
if (!localStorage) return;
|
|
||||||
expect(localStorage.getItem("token")).toEqual(TOKEN_STRING);
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Handle server error", async () => {
|
|
||||||
auth.setSignInResponse({ message: "SERVER-ERROR", error: "ERR" });
|
|
||||||
fireEvent.click(screen.getByTestId("signIn"));
|
fireEvent.click(screen.getByTestId("signIn"));
|
||||||
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
|
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
|
await waitFor(() =>
|
||||||
auth.setSignUpResponse({});
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
|
);
|
||||||
|
|
||||||
|
checkBaseResults("", "false", TOKEN_STRING);
|
||||||
|
checkTokenResults(timer, TOKEN_STRING);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,15 +132,21 @@ describe("UseContext", () => {
|
|||||||
<div data-testid="error">{error}</div>
|
<div data-testid="error">{error}</div>
|
||||||
<div data-testid="fetching">{fetching.toString()}</div>
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
<div data-testid="token">{JSON.stringify(token)}</div>
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
|
<button data-testid="signIn" onClick={() => signIn("TEST_CODE")} />
|
||||||
<button data-testid="signOut" onClick={() => signOut()}/>
|
<button data-testid="signOut" onClick={() => signOut()} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
render(<UserProvider><TestComp /></UserProvider>);
|
render(
|
||||||
|
<UserProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
auth.setSignInResponse(TEST_TOKEN);
|
auth.setSignInResponse(TEST_TOKEN);
|
||||||
fireEvent.click(screen.getByTestId("signIn"));
|
fireEvent.click(screen.getByTestId("signIn"));
|
||||||
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -159,11 +156,79 @@ describe("UseContext", () => {
|
|||||||
|
|
||||||
it("Token cleared", () => {
|
it("Token cleared", () => {
|
||||||
fireEvent.click(screen.getByTestId("signOut"));
|
fireEvent.click(screen.getByTestId("signOut"));
|
||||||
expect(screen.getByTestId("error").innerHTML).toEqual("");
|
|
||||||
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
|
checkBaseResults("", "false", "null");
|
||||||
expect(screen.getByTestId("token").innerHTML).toEqual("null");
|
|
||||||
if (!localStorage) return;
|
if (!localStorage) return;
|
||||||
expect(localStorage.getItem('token')).toBeNull();
|
expect(localStorage.getItem("token")).toBeNull();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
describe("Refresh", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { refresh, error, token, fetching } = useUserContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="error">{error}</div>
|
||||||
|
<div data-testid="fetching">{fetching.toString()}</div>
|
||||||
|
<div data-testid="token">{JSON.stringify(token)}</div>
|
||||||
|
<button data-testid="refreshNoToken" onClick={() => refresh("")} />
|
||||||
|
<button
|
||||||
|
data-testid="refreshInvalidToken"
|
||||||
|
onClick={() => refresh("INVALID_TOKEN")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
data-testid="refreshValidToken"
|
||||||
|
onClick={() => refresh("TEST_TOKEN")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<UserProvider>
|
||||||
|
<TestComp />
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", () => {
|
||||||
|
checkBaseResults("", "false", "null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("No refresh token", () => {
|
||||||
|
fireEvent.click(screen.getByTestId("refreshNoToken"));
|
||||||
|
checkBaseResults("Refresh error. Token required", "false", "null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Invalid refresh token", async () => {
|
||||||
|
setupRefreshEnv(INVALID_TOKEN_RESPONSE, false);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("refreshInvalidToken"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
|
);
|
||||||
|
|
||||||
|
checkBaseResults("Refresh error. Bad Request Message", "false", "null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Valid refresh token", async () => {
|
||||||
|
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
|
||||||
|
const timer = getTimerWorker();
|
||||||
|
|
||||||
|
setupRefreshEnv(TEST_TOKEN, true);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("refreshValidToken"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("fetching").innerHTML).toEqual("true")
|
||||||
|
);
|
||||||
|
|
||||||
|
checkBaseResults("", "false", TOKEN_STRING);
|
||||||
|
checkTokenResults(timer, TOKEN_STRING);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
65
src/components/Contexts/VehicleContext.jsx
Normal file
65
src/components/Contexts/VehicleContext.jsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import api from "../../services/vehicles";
|
||||||
|
|
||||||
|
const VehicleContext = React.createContext();
|
||||||
|
|
||||||
|
const validateAdd = (vehicle) => {
|
||||||
|
if (vehicle === null) {
|
||||||
|
throw new Error("No vehicle data");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vehicle.vin || vehicle.vin.length === 0) {
|
||||||
|
throw new Error("VIN required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vehicle.vin.length > 17) {
|
||||||
|
throw new Error("VIN cannot be larger than 17 characters");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VehicleProvider = ({ children }) => {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [vehicles, setVehicles] = useState([]);
|
||||||
|
|
||||||
|
const getVehicles = async (search, token) => {
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
const result = await api.getVehicles(search, token);
|
||||||
|
if (result.error) {
|
||||||
|
setVehicles([]);
|
||||||
|
throw new Error(`Get vehicles error. ${result.message}`);
|
||||||
|
} else {
|
||||||
|
setVehicles(result.data);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVehicle = async (vehicle, token) => {
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
validateAdd(vehicle);
|
||||||
|
const result = await api.addVehicle(vehicle, token);
|
||||||
|
if (result.error) throw new Error(`Add vehicle error. ${result.message}`);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VehicleContext.Provider
|
||||||
|
value={{
|
||||||
|
busy,
|
||||||
|
vehicles,
|
||||||
|
getVehicles,
|
||||||
|
addVehicle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</VehicleContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVehicleContext = () => useContext(VehicleContext);
|
||||||
144
src/components/Contexts/VehicleContext.test.jsx
Normal file
144
src/components/Contexts/VehicleContext.test.jsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
jest.mock("../../services/vehicles");
|
||||||
|
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
cleanup,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
import { VehicleProvider, useVehicleContext } from "./VehicleContext";
|
||||||
|
import { StatusProvider, useStatusContext } from "./StatusContext";
|
||||||
|
|
||||||
|
const checkVehicleResults = (error, busy, vehicles) => {
|
||||||
|
checkBaseResults(error, busy);
|
||||||
|
expect(screen.getByTestId("vehicles").innerHTML).toEqual(vehicles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkBaseResults = (error, busy) => {
|
||||||
|
expect(screen.getByTestId("error").innerHTML).toEqual(error);
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual(busy);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("VehicleContext", () => {
|
||||||
|
describe("getVehicles", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { busy, error, vehicles, getVehicles } = useVehicleContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="error">{error}</div>
|
||||||
|
<div data-testid="busy">{busy.toString()}</div>
|
||||||
|
<div data-testid="vehicles">{JSON.stringify(vehicles)}</div>
|
||||||
|
<button
|
||||||
|
data-testid="getVehicles"
|
||||||
|
onClick={() => getVehicles(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<VehicleProvider>
|
||||||
|
<TestComp />
|
||||||
|
</VehicleProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", () => {
|
||||||
|
checkVehicleResults("", "false", "[]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getVehicles", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("getVehicles"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("vehicles").innerHTML).not.toBe("[]")
|
||||||
|
);
|
||||||
|
checkVehicleResults("", "false", JSON.stringify(expectedVehicleData));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AddVehicles", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const TestComp = () => {
|
||||||
|
const { busy, addVehicle } = useVehicleContext();
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
|
const add = async (data) => {
|
||||||
|
try {
|
||||||
|
await addVehicle(data);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-testid="error">{message}</div>
|
||||||
|
<div data-testid="busy">{busy.toString()}</div>
|
||||||
|
<button data-testid="addVehiclesNull" onClick={() => add(null)} />
|
||||||
|
<button data-testid="addVehiclesNoVIN" onClick={() => add({})} />
|
||||||
|
<button
|
||||||
|
data-testid="addVehicles"
|
||||||
|
onClick={() => add({ vin: "XXXXXXXXXXX" })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<StatusProvider>
|
||||||
|
<VehicleProvider>
|
||||||
|
<TestComp />
|
||||||
|
</VehicleProvider>
|
||||||
|
</StatusProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Initial state", () => {
|
||||||
|
checkBaseResults("", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addVehiclesNull", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("addVehiclesNull"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||||
|
);
|
||||||
|
checkBaseResults("No vehicle data", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addVehiclesNoVIN", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("addVehiclesNoVIN"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||||
|
);
|
||||||
|
checkBaseResults("VIN required", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addVehicles", async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("addVehicles"));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByTestId("busy").innerHTML).toEqual("false")
|
||||||
|
);
|
||||||
|
checkBaseResults("", "false");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedVehicleData = {
|
||||||
|
data: [
|
||||||
|
{ vin: "3C4PDCBG0ET127145" },
|
||||||
|
{ vin: "1G1FP87S3GN100062" },
|
||||||
|
{ vin: "1HGCG325XYA062256" },
|
||||||
|
{ vin: "1J4GZ78YXWC160024" },
|
||||||
|
{ vin: "2C3CCAAG8CH222800" },
|
||||||
|
{ vin: "KNADM4A39C6028108" },
|
||||||
|
{ vin: "1G11C5SL9FF153507" },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -3,19 +3,20 @@ import React from "react";
|
|||||||
let uploading = false;
|
let uploading = false;
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
let status = null;
|
let status = null;
|
||||||
|
let files = null;
|
||||||
|
|
||||||
export const FileUploadProvider = ({ children }) => {
|
export const FileUploadProvider = ({ children }) => {
|
||||||
return (
|
return <div data-testid="mocked-fileuploadprovider">{children}</div>;
|
||||||
<div data-testid="mocked-fileuploadprovider">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFileUploadContext = () => ({
|
export const useFileUploadContext = () => ({
|
||||||
uploading,
|
uploading,
|
||||||
progress,
|
progress,
|
||||||
status,
|
status,
|
||||||
|
files,
|
||||||
upload: jest.fn(),
|
upload: jest.fn(),
|
||||||
cancel: jest.fn(),
|
cancel: jest.fn(),
|
||||||
|
setFiles: jest.fn((value) => {
|
||||||
|
files = value;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
let token = null;
|
let token = null;
|
||||||
let fetching = false;
|
let fetching = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
let signInResp = {};
|
||||||
|
let authorizeURL = "https://cognito.com/authorize?redirect=https://example.com/callback";
|
||||||
|
let logoutURL = "https://cognito.com/logout?redirect=https://example.com/callback";
|
||||||
|
|
||||||
export const UserProvider = ({ children }) => {
|
export const UserProvider = ({ children }) => {
|
||||||
return (
|
return <div data-testid="mocked-userprovider">{children}</div>;
|
||||||
<div data-testid="mocked-userprovider">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUserContext = () => ({
|
export const useUserContext = () => ({
|
||||||
token,
|
token,
|
||||||
fetching,
|
fetching,
|
||||||
error,
|
error,
|
||||||
setError: jest.fn(),
|
signIn: jest.fn(() => signInResp),
|
||||||
signIn: jest.fn(),
|
|
||||||
signUp: jest.fn(),
|
|
||||||
signOut: jest.fn(),
|
signOut: jest.fn(),
|
||||||
|
getAuthorizeURL: jest.fn(() => authorizeURL),
|
||||||
|
getLogoutURL: jest.fn(() => logoutURL),
|
||||||
|
setError: jest.fn((value) => {
|
||||||
|
error = value;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setToken = (val) => {
|
export const setToken = (val) => {
|
||||||
|
|||||||
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal file
24
src/components/Contexts/__mocks__/VehicleContext.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
let busy = false;
|
||||||
|
let vehicles = [];
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
export const VehicleProvider = ({ children }) => {
|
||||||
|
return <div data-testid="mocked-vehicleprovider">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVehicleContext = () => ({
|
||||||
|
busy,
|
||||||
|
vehicles,
|
||||||
|
getVehicles: jest.fn(() => vehicles),
|
||||||
|
addVehicle: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setBusy = (val) => {
|
||||||
|
busy = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setVehicles = (val) => {
|
||||||
|
vehicles = val;
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
import { Typography } from "@material-ui/core";
|
||||||
|
|
||||||
export default class ErrorBoundary extends Component {
|
export default class ErrorBoundary extends Component {
|
||||||
state = {
|
state = {
|
||||||
error: '',
|
error: "",
|
||||||
errorInfo: '',
|
errorInfo: "",
|
||||||
hasError: false,
|
hasError: false,
|
||||||
};
|
};
|
||||||
static getDerivedStateFromError(error) {
|
static getDerivedStateFromError(error) {
|
||||||
@@ -14,10 +15,15 @@ export default class ErrorBoundary extends Component {
|
|||||||
this.setState({ errorInfo });
|
this.setState({ errorInfo });
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) return (<h1>Oops. An Error Occured</h1>);
|
if (this.state.hasError)
|
||||||
return this.props.children;
|
return (
|
||||||
|
<Typography variant="h3" align="center">
|
||||||
|
Oops. An React JS Error Occured.
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
return this.props.children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ErrorBoundary.propTypes = {
|
ErrorBoundary.propTypes = {
|
||||||
children: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
|
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
jest.mock("../Contexts/UserContext");
|
jest.mock("../Contexts/UserContext");
|
||||||
jest.mock("../Contexts/FileUploadContext");
|
jest.mock("../Contexts/FileUploadContext");
|
||||||
|
jest.mock("../Contexts/VehicleContext");
|
||||||
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { render, cleanup } from "@testing-library/react"
|
import { render, cleanup, waitFor } from "@testing-library/react";
|
||||||
import FileUploadForm from './index';
|
import FileUploadForm from "./index";
|
||||||
|
import { setToken } from "../Contexts/UserContext";
|
||||||
|
import { StatusProvider } from "../Contexts/StatusContext";
|
||||||
|
|
||||||
describe("File Upload Form", () => {
|
describe("File Upload Form", () => {
|
||||||
|
it("Should render", async () => {
|
||||||
it("Should render", () => {
|
setToken({ idToken: { jwtToken: "TEST" } });
|
||||||
const { container } = render(<BrowserRouter><FileUploadForm /></BrowserRouter>);
|
const { container } = render(<StatusProvider><BrowserRouter><FileUploadForm /></BrowserRouter></StatusProvider>);
|
||||||
|
await waitFor(() => {});
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
cleanup();
|
cleanup();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,24 +2,238 @@
|
|||||||
|
|
||||||
exports[`File Upload Form Should render 1`] = `
|
exports[`File Upload Form Should render 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<main
|
<div
|
||||||
class="MuiContainer-root MuiContainer-maxWidthXs"
|
data-testid="mocked-vehicleprovider"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="makeStyles-paper-1"
|
data-testid="mocked-fileuploadprovider"
|
||||||
>
|
>
|
||||||
<h1
|
|
||||||
class="MuiTypography-root MuiTypography-h5"
|
|
||||||
>
|
|
||||||
Upload file
|
|
||||||
</h1>
|
|
||||||
<div
|
<div
|
||||||
data-testid="mocked-fileuploadprovider"
|
class="makeStyles-paper-1"
|
||||||
>
|
>
|
||||||
|
<h1
|
||||||
|
class="MuiTypography-root MuiTypography-h5"
|
||||||
|
>
|
||||||
|
Upload Update Package
|
||||||
|
</h1>
|
||||||
<form
|
<form
|
||||||
|
action="{onSubmit}"
|
||||||
class="makeStyles-form-3"
|
class="makeStyles-form-3"
|
||||||
novalidate=""
|
novalidate=""
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="packagename"
|
||||||
|
id="packagename-label"
|
||||||
|
>
|
||||||
|
Package name
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="packagename"
|
||||||
|
maxlength="255"
|
||||||
|
name="packagename"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Package name
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
||||||
|
data-shrink="false"
|
||||||
|
for="version"
|
||||||
|
id="version-label"
|
||||||
|
>
|
||||||
|
Version
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="version"
|
||||||
|
maxlength="255"
|
||||||
|
name="version"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Version
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||||
|
data-shrink="false"
|
||||||
|
for="description"
|
||||||
|
id="description-label"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMultiline MuiOutlinedInput-inputMultiline"
|
||||||
|
id="description"
|
||||||
|
maxlength="5120"
|
||||||
|
name="description"
|
||||||
|
placeholder="Package description"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Description
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||||
|
data-shrink="false"
|
||||||
|
for="releasenotes"
|
||||||
|
id="releasenotes-label"
|
||||||
|
>
|
||||||
|
Release Notes URL
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input"
|
||||||
|
id="releasenotes"
|
||||||
|
maxlength="1024"
|
||||||
|
name="releasenotes"
|
||||||
|
placeholder="Release Notes URL"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="PrivateNotchedOutline-legendLabelled-23"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Release Notes URL
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root makeStyles-formControl-5 MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined"
|
||||||
|
data-shrink="false"
|
||||||
|
for="vehicles"
|
||||||
|
>
|
||||||
|
Vehicles
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiInput-root MuiInput-underline makeStyles-menuProps-8 MuiInputBase-formControl MuiInput-formControl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-labelledby="vehicles"
|
||||||
|
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiInput-input"
|
||||||
|
id="vehicles"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSelect-nativeInput"
|
||||||
|
id="select-multiple-chip"
|
||||||
|
name="vehicles"
|
||||||
|
placeholder="Select vehicles"
|
||||||
|
tabindex="-1"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
|
||||||
|
focusable="false"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 10l5 5 5-5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="MuiDropzoneArea-root"
|
class="MuiDropzoneArea-root"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -27,7 +241,6 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
<input
|
<input
|
||||||
accept=""
|
accept=""
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
multiple=""
|
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -52,31 +265,23 @@ exports[`File Upload Form Should render 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-item"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="MuiButtonBase-root MuiButton-root MuiButton-text"
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
type="button"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="MuiButton-label"
|
class="MuiButton-label"
|
||||||
>
|
>
|
||||||
Sign Out
|
Submit
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,46 +1,223 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Container, CssBaseline, Grid, Typography } from "@material-ui/core";
|
import {
|
||||||
import { DropzoneAreaBase } from "material-ui-dropzone";
|
Button,
|
||||||
import { useUserContext } from "../Contexts/UserContext";
|
Chip,
|
||||||
import { useFileUploadContext, FileUploadProvider } from "../Contexts/FileUploadContext";
|
FormControl,
|
||||||
|
Input,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { DropzoneArea } from "material-ui-dropzone";
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../Contexts/StatusContext";
|
||||||
|
import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
|
||||||
|
import {
|
||||||
|
useFileUploadContext,
|
||||||
|
FileUploadProvider,
|
||||||
|
} from "../Contexts/FileUploadContext";
|
||||||
import ModalProgressBar from "../ModalProgressBar";
|
import ModalProgressBar from "../ModalProgressBar";
|
||||||
import useStyles from "../Styles";
|
import useStyles from "../useStyles";
|
||||||
|
import menuItemStyle from "../menuItemStyle";
|
||||||
|
|
||||||
const FileUploadZone = ({ classes }) => {
|
const FileUploadZone = ({ classes, token }) => {
|
||||||
const { uploading, progress, status, upload, cancel } = useFileUploadContext();
|
const { setFiles } = useFileUploadContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={classes.form} noValidate>
|
<>
|
||||||
<DropzoneAreaBase
|
<DropzoneArea
|
||||||
maxFileSize={5e+7}
|
id="dropzone"
|
||||||
|
showPreviews={true}
|
||||||
|
showPreviewsInDropzone={false}
|
||||||
|
useChipsForPreview
|
||||||
|
previewGridProps={{ container: { spacing: 1, direction: "row" } }}
|
||||||
|
previewChipProps={{ classes: { root: classes.previewChip } }}
|
||||||
|
previewText="Selected files"
|
||||||
|
maxFileSize={1e9}
|
||||||
|
filesLimit={1}
|
||||||
showAlerts={false}
|
showAlerts={false}
|
||||||
onAdd={upload}
|
onChange={(files) => setFiles(files)}
|
||||||
|
onDelete={(files) => setFiles(files)}
|
||||||
|
onDropRejected={(files) => {
|
||||||
|
setMessage(`Rejected ${files[0].name} too large`);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ModalProgressBar uploading={uploading} progress={progress} onCancel={cancel} status={status} />
|
<ModalProgressBar />
|
||||||
</form>
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { uploading, upload, files } = useFileUploadContext();
|
||||||
|
const { token } = useUserContext();
|
||||||
|
const { getVehicles, vehicles } = useVehicleContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const [selectedVehicles, setSelectedVehicles] = useState([]);
|
||||||
|
const theme = useTheme();
|
||||||
|
const classes = useStyles();
|
||||||
|
const packagenameEl = useRef(null);
|
||||||
|
const versionEl = useRef(null);
|
||||||
|
const descEl = useRef(null);
|
||||||
|
const releasenotesEl = useRef(null);
|
||||||
|
const handleVehiclesChange = (event) => {
|
||||||
|
setSelectedVehicles(event.target.value);
|
||||||
|
};
|
||||||
|
const onSubmit = async (event) => {
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
const {
|
||||||
|
idToken: { jwtToken: authToken },
|
||||||
|
} = token;
|
||||||
|
const formData = {
|
||||||
|
packagename: packagenameEl.current.value,
|
||||||
|
version: versionEl.current.value,
|
||||||
|
description: descEl.current.value,
|
||||||
|
releasenotes: releasenotesEl.current.value,
|
||||||
|
vehicles: selectedVehicles,
|
||||||
|
};
|
||||||
|
|
||||||
|
await upload(formData, authToken, files);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const {
|
||||||
|
idToken: { jwtToken: authToken },
|
||||||
|
} = token;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await getVehicles(null, authToken);
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Upload Update Package
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
|
<TextField
|
||||||
|
id="packagename"
|
||||||
|
name="packagename"
|
||||||
|
label="Package name"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "255",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
inputRef={packagenameEl}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="version"
|
||||||
|
name="version"
|
||||||
|
label="Version"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "255",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
inputRef={versionEl}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "5120",
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
placeholder="Package description"
|
||||||
|
inputRef={descEl}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="releasenotes"
|
||||||
|
name="releasenotes"
|
||||||
|
label="Release Notes URL"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "1024",
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Release Notes URL"
|
||||||
|
inputRef={releasenotesEl}
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
className={classes.formControl}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel htmlFor="vehicles">Vehicles</InputLabel>
|
||||||
|
<Select
|
||||||
|
label="Vehicles"
|
||||||
|
placeholder="Select vehicles"
|
||||||
|
id="vehicles"
|
||||||
|
name="vehicles"
|
||||||
|
multiple
|
||||||
|
className={classes.menuProps}
|
||||||
|
onChange={handleVehiclesChange}
|
||||||
|
value={selectedVehicles}
|
||||||
|
input={<Input id="select-multiple-chip" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<div className={classes.chips}>
|
||||||
|
{selected.map((value) => (
|
||||||
|
<Chip key={value} label={value} className={classes.chip} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{vehicles.map((vehicle) => (
|
||||||
|
<MenuItem
|
||||||
|
key={vehicle.vin}
|
||||||
|
value={vehicle.vin}
|
||||||
|
style={menuItemStyle(vehicle, selectedVehicles, theme)}
|
||||||
|
>
|
||||||
|
{vehicle.vin}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FileUploadZone classes={classes} />
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={uploading}
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{uploading ? "Uploading..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileUploadForm() {
|
export default function FileUploadForm() {
|
||||||
const { signOut } = useUserContext();
|
return (
|
||||||
const classes = useStyles();
|
<VehicleProvider>
|
||||||
|
<FileUploadProvider>
|
||||||
return (
|
<MainForm />
|
||||||
<Container component="main" maxWidth="xs">
|
</FileUploadProvider>
|
||||||
<CssBaseline />
|
</VehicleProvider>
|
||||||
<div className={classes.paper}>
|
);
|
||||||
<Typography component="h1" variant="h5">
|
}
|
||||||
Upload file
|
|
||||||
</Typography>
|
|
||||||
<FileUploadProvider>
|
|
||||||
<FileUploadZone classes={classes} />
|
|
||||||
</FileUploadProvider>
|
|
||||||
<Grid container>
|
|
||||||
<Grid item >
|
|
||||||
<Button onClick={signOut}>Sign Out</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
105
src/components/Layouts/MenuDrawer.jsx
Normal file
105
src/components/Layouts/MenuDrawer.jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useTheme } from "@material-ui/core/styles";
|
||||||
|
import Drawer from "@material-ui/core/Drawer";
|
||||||
|
import AppBar from "@material-ui/core/AppBar";
|
||||||
|
import Toolbar from "@material-ui/core/Toolbar";
|
||||||
|
import Typography from "@material-ui/core/Typography";
|
||||||
|
import Divider from "@material-ui/core/Divider";
|
||||||
|
import IconButton from "@material-ui/core/IconButton";
|
||||||
|
import MenuIcon from "@material-ui/icons/Menu";
|
||||||
|
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
||||||
|
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
||||||
|
|
||||||
|
import SideMenu from "./SideMenu";
|
||||||
|
import useStyles from "../useStyles";
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { Button, Container } from "@material-ui/core";
|
||||||
|
|
||||||
|
export default function MenuDrawer({ children }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
const { signOut, token } = useUserContext();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleDrawerOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
className={clsx(classes.appBar, {
|
||||||
|
[classes.appBarShift]: open && token !== null,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
{token !== null && (
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
onClick={handleDrawerOpen}
|
||||||
|
edge="start"
|
||||||
|
className={clsx(
|
||||||
|
classes.menuButton,
|
||||||
|
open && classes.hide && token !== null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography variant="h6" noWrap>
|
||||||
|
Fisker OTA Portal
|
||||||
|
</Typography>
|
||||||
|
{token !== null && (
|
||||||
|
<Button
|
||||||
|
color="inherit"
|
||||||
|
onClick={signOut}
|
||||||
|
className={classes.rightToolbar}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
{token !== null && (
|
||||||
|
<Drawer
|
||||||
|
className={classes.drawer}
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
classes={{
|
||||||
|
paper: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={classes.drawerHeader}>
|
||||||
|
<IconButton onClick={handleDrawerClose}>
|
||||||
|
{theme.direction === "ltr" ? (
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
) : (
|
||||||
|
<ChevronRightIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<SideMenu />
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
<main
|
||||||
|
className={clsx(classes.content, {
|
||||||
|
[classes.contentShift]: open && token !== null,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={classes.drawerHeader} />
|
||||||
|
<Container component="main" maxWidth="md">
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/components/Layouts/SideMenu.jsx
Normal file
18
src/components/Layouts/SideMenu.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { List } from "@material-ui/core";
|
||||||
|
import ListItemLink from "../ListItemLink";
|
||||||
|
|
||||||
|
export default function SideMenu() {
|
||||||
|
const menuData = [
|
||||||
|
{ label: "Upload Update Package", to: "/home" },
|
||||||
|
{ label: "Add Vehicles", to: "/vehicle-add" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{menuData.map((item, index) => (
|
||||||
|
<ListItemLink key={index} primary={item.label} to={item.to} />
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/components/ListItemLink.jsx
Normal file
35
src/components/ListItemLink.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import ListItem from "@material-ui/core/ListItem";
|
||||||
|
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||||
|
import ListItemText from "@material-ui/core/ListItemText";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
function ListItemLink(props) {
|
||||||
|
const { icon, primary, to } = props;
|
||||||
|
|
||||||
|
const renderLink = React.useMemo(
|
||||||
|
() =>
|
||||||
|
React.forwardRef((itemProps, ref) => (
|
||||||
|
<RouterLink to={to} ref={ref} {...itemProps} />
|
||||||
|
)),
|
||||||
|
[to]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<ListItem button component={renderLink}>
|
||||||
|
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
|
||||||
|
<ListItemText primary={primary} />
|
||||||
|
</ListItem>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItemLink.propTypes = {
|
||||||
|
icon: PropTypes.element,
|
||||||
|
primary: PropTypes.string.isRequired,
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListItemLink;
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Snackbar } from "@material-ui/core";
|
import { Snackbar } from "@material-ui/core";
|
||||||
import { useUserContext } from './Contexts/UserContext';
|
import { useStatusContext } from "./Contexts/StatusContext";
|
||||||
|
import { useUserContext } from "./Contexts/UserContext";
|
||||||
|
|
||||||
export const MessageBar = () => {
|
export const MessageBar = () => {
|
||||||
|
const { message, setMessage } = useStatusContext();
|
||||||
const { error, setError } = useUserContext();
|
const { error, setError } = useUserContext();
|
||||||
const open = (error !== null);
|
const open = message !== null || error !== null;
|
||||||
|
const msg = message || error;
|
||||||
|
|
||||||
return (<Snackbar
|
return (
|
||||||
open={open}
|
<Snackbar
|
||||||
message={error}
|
open={open}
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
message={msg}
|
||||||
autoHideDuration={10000}
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||||
onClose={() => setError(null)}/>)
|
autoHideDuration={10000}
|
||||||
}
|
onClose={() => {
|
||||||
|
setMessage(null);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Modal from '@material-ui/core/Modal';
|
import Modal from "@material-ui/core/Modal";
|
||||||
|
|
||||||
import { Button, LinearProgress } from "@material-ui/core";
|
import { Button, LinearProgress } from "@material-ui/core";
|
||||||
|
import { useFileUploadContext } from "../Contexts/FileUploadContext";
|
||||||
|
|
||||||
const getModalStyle = () => {
|
const getModalStyle = () => {
|
||||||
const top = 30;
|
const top = 30;
|
||||||
@@ -21,24 +22,36 @@ const getModalStyle = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalProgressBar = ({ onCancel, uploading, progress, status }) => {
|
const ModalProgressBar = () => {
|
||||||
|
const {
|
||||||
|
uploading,
|
||||||
|
progress,
|
||||||
|
status,
|
||||||
|
linkURL,
|
||||||
|
cancel,
|
||||||
|
} = useFileUploadContext();
|
||||||
|
|
||||||
const modalStyle = getModalStyle();
|
const modalStyle = getModalStyle();
|
||||||
const onClickCancel = () => {
|
const onClickCancel = cancel;
|
||||||
if (onCancel) onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={uploading}>
|
<Modal open={uploading}>
|
||||||
<div style={modalStyle}>
|
<div style={modalStyle}>
|
||||||
{status && <p>{status}</p>}
|
{status && <p>{status}</p>}
|
||||||
|
{linkURL && (
|
||||||
|
<p>
|
||||||
|
<a href={linkURL} target="_blank" rel="noreferrer">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<LinearProgress variant="determinate" value={progress} />
|
<LinearProgress variant="determinate" value={progress} />
|
||||||
<Button onClick={onClickCancel}>
|
<Button onClick={onClickCancel}>
|
||||||
{ progress < 100 ? "Cancel" : "Done" }
|
{progress === 100 || progress === -1 ? "Done" : "Cancel"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default ModalProgressBar;
|
export default ModalProgressBar;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from "react-router-dom";
|
||||||
|
|
||||||
export const TYPES = {
|
export const TYPES = {
|
||||||
PUBLIC: 0,
|
PUBLIC: 0,
|
||||||
@@ -9,10 +9,9 @@ export const TYPES = {
|
|||||||
|
|
||||||
export const AuthRoute = ({ token, type, ...others }) => {
|
export const AuthRoute = ({ token, type, ...others }) => {
|
||||||
if (!token && type === TYPES.PROTECTED) {
|
if (!token && type === TYPES.PROTECTED) {
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
} else if (token && type === TYPES.GUEST) {
|
||||||
else if (token && type === TYPES.GUEST) {
|
|
||||||
return <Redirect to="/home" />;
|
return <Redirect to="/home" />;
|
||||||
}
|
}
|
||||||
return <Route render {...others} />;
|
return <Route render {...others} />;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from "react-router-dom";
|
||||||
import { useUserContext } from '../Contexts/UserContext';
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { useStatusContext } from "../Contexts/StatusContext";
|
||||||
|
|
||||||
export const ProtectedRoute = ({ render, ...others }) => {
|
export const ProtectedRoute = ({ render, ...others }) => {
|
||||||
const context = useUserContext();
|
const { token } = useUserContext();
|
||||||
const { token, setError } = context;
|
const { setMessage } = useStatusContext();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setError('Please sign in to access');
|
setMessage("Please sign in to access");
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
return <Route render {...others} />;
|
return <Route render {...others} />;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from "react";
|
||||||
import {
|
import { Switch } from "react-router-dom";
|
||||||
BrowserRouter,
|
|
||||||
Switch,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AuthRoute, TYPES } from '../Routes/AuthRoute'
|
import { AuthRoute, TYPES } from "../Routes/AuthRoute";
|
||||||
import { MessageBar } from '../MessageBar';
|
import { MessageBar } from "../MessageBar";
|
||||||
import { useUserContext } from '../Contexts/UserContext';
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
|
||||||
const SignInForm = React.lazy(() => import('../SignInForm'));
|
const SSOForm = React.lazy(() => import("../SSOForm"));
|
||||||
const SignUpForm = React.lazy(() => import('../SignUpForm'));
|
const FileUploadForm = React.lazy(() => import("../FileUploadForm"));
|
||||||
const FileUploadForm = React.lazy(() => import('../FileUploadForm'));
|
const VehicleAddForm = React.lazy(() => import("../VehicleAddForm"));
|
||||||
const PageNotFound = React.lazy(() => import('../404'));
|
const PageNotFound = React.lazy(() => import("../404"));
|
||||||
|
|
||||||
const SiteRoutes = () => {
|
const SiteRoutes = () => {
|
||||||
const { token } = useUserContext();
|
const { token } = useUserContext();
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={"Loading..."}>
|
<Suspense fallback={"Loading..."}>
|
||||||
<MessageBar />
|
<MessageBar />
|
||||||
<BrowserRouter>
|
<Switch>
|
||||||
<Switch>
|
<AuthRoute
|
||||||
<AuthRoute path="/" exact render={() => <SignInForm />} type={TYPES.GUEST} token={token} />
|
path="/"
|
||||||
<AuthRoute path="/signup" exact render={() => <SignUpForm />} type={TYPES.GUEST} token={token} />
|
exact
|
||||||
<AuthRoute path="/home" render={() => <FileUploadForm />} type={TYPES.PROTECTED} token={token} />
|
render={() => <SSOForm />}
|
||||||
<PageNotFound />
|
type={TYPES.GUEST}
|
||||||
</Switch>
|
token={token}
|
||||||
</BrowserRouter>
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/home"
|
||||||
|
render={() => <FileUploadForm />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/vehicle-add"
|
||||||
|
render={() => <VehicleAddForm />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
<PageNotFound />
|
||||||
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default SiteRoutes;
|
export default SiteRoutes;
|
||||||
19
src/components/SSOForm/SSOForm.test.js
Normal file
19
src/components/SSOForm/SSOForm.test.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
jest.mock("../Contexts/UserContext");
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { render, cleanup } from "@testing-library/react";
|
||||||
|
import SSOForm from "./index";
|
||||||
|
|
||||||
|
describe("Sign In Form", () => {
|
||||||
|
it("Should render", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<SSOForm />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/components/SSOForm/__snapshots__/SSOForm.test.js.snap
Normal file
26
src/components/SSOForm/__snapshots__/SSOForm.test.js.snap
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Sign In Form Should render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="makeStyles-paper-1"
|
||||||
|
style="justify-content: center;"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary"
|
||||||
|
href="https://cognito.com/authorize?redirect=https://example.com/callback"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="MuiButton-label"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
37
src/components/SSOForm/index.jsx
Normal file
37
src/components/SSOForm/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { Button } from "@material-ui/core";
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import useStyles from "../useStyles";
|
||||||
|
|
||||||
|
const getCode = (search) => {
|
||||||
|
if (!search) return null;
|
||||||
|
const s = new URLSearchParams(search);
|
||||||
|
return s.get("code");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SignInForm() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { getAuthorizeURL, signIn, fetching } = useUserContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const code = getCode(document.location.search);
|
||||||
|
if (!code) return;
|
||||||
|
signIn(code);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper} style={{ justifyContent: "center" }}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
href={getAuthorizeURL()}
|
||||||
|
disabled={fetching}
|
||||||
|
>
|
||||||
|
{fetching ? "Please wait..." : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
jest.mock("../Contexts/UserContext");
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { render, cleanup } from "@testing-library/react"
|
|
||||||
import SignInForm from './index';
|
|
||||||
|
|
||||||
describe("Sign In Form", () => {
|
|
||||||
|
|
||||||
it("Should render", () => {
|
|
||||||
const { container } = render(<BrowserRouter><SignInForm /></BrowserRouter>);
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
cleanup();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Sign In Form Should render 1`] = `
|
|
||||||
<div>
|
|
||||||
<main
|
|
||||||
class="MuiContainer-root MuiContainer-maxWidthXs"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="makeStyles-paper-1"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="MuiTypography-root MuiTypography-h5"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</h1>
|
|
||||||
<form
|
|
||||||
action="{onSubmit}"
|
|
||||||
class="makeStyles-form-3"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
|
|
||||||
data-shrink="true"
|
|
||||||
for="email"
|
|
||||||
id="email-label"
|
|
||||||
>
|
|
||||||
Email Address
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
autocomplete="email"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
required=""
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-7 PrivateNotchedOutline-legendNotched-8"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Email Address
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
|
||||||
data-shrink="false"
|
|
||||||
for="password"
|
|
||||||
id="password-label"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
autocomplete="current-password"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required=""
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-7"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Password
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
|
|
||||||
tabindex="0"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="MuiButton-label"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="MuiTouchRipple-root"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-item"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
|
|
||||||
href="/signup"
|
|
||||||
>
|
|
||||||
Don't have an account? Sign Up
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import React, { useRef } from 'react';
|
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
|
||||||
import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core';
|
|
||||||
import { useUserContext } from '../Contexts/UserContext';
|
|
||||||
import useStyles from '../Styles';
|
|
||||||
|
|
||||||
export default function SignInForm() {
|
|
||||||
const classes = useStyles();
|
|
||||||
const emailEl = useRef(null);
|
|
||||||
const passwordEl = useRef(null);
|
|
||||||
const { fetching, signIn, setError } = useUserContext();
|
|
||||||
const onSubmit = async (event) => {
|
|
||||||
try {
|
|
||||||
event.preventDefault();
|
|
||||||
const username = emailEl.current.value;
|
|
||||||
const password = passwordEl.current.value;
|
|
||||||
await signIn(username, password);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container component="main" maxWidth="xs">
|
|
||||||
<CssBaseline />
|
|
||||||
<div className={classes.paper}>
|
|
||||||
<Typography component="h1" variant="h5">
|
|
||||||
Sign in
|
|
||||||
</Typography>
|
|
||||||
<form className={classes.form} noValidate action="{onSubmit}">
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
id="email"
|
|
||||||
label="Email Address"
|
|
||||||
name="email"
|
|
||||||
autoComplete="email"
|
|
||||||
autoFocus
|
|
||||||
inputRef={emailEl}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
inputRef={passwordEl}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={fetching}
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
className={classes.submit}
|
|
||||||
onClick={onSubmit}
|
|
||||||
>
|
|
||||||
{ fetching ? "Signing In..." : "Sign In" }
|
|
||||||
</Button>
|
|
||||||
<Grid container>
|
|
||||||
<Grid item>
|
|
||||||
<Link component={RouterLink} to="/signup" variant="body2">
|
|
||||||
{"Don't have an account? Sign Up"}
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
jest.mock("../Contexts/UserContext");
|
|
||||||
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { render, cleanup } from "@testing-library/react"
|
|
||||||
import SignUpForm from './index';
|
|
||||||
|
|
||||||
describe("Sign Up Form", () => {
|
|
||||||
|
|
||||||
it("Should render", () => {
|
|
||||||
const { container } = render(<BrowserRouter><SignUpForm /></BrowserRouter>);
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
cleanup();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Sign Up Form Should render 1`] = `
|
|
||||||
<div>
|
|
||||||
<main
|
|
||||||
class="MuiContainer-root MuiContainer-maxWidthXs"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="makeStyles-paper-1"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
class="MuiTypography-root MuiTypography-h5"
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</h1>
|
|
||||||
<form
|
|
||||||
class="makeStyles-form-3"
|
|
||||||
novalidate=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
|
|
||||||
data-shrink="true"
|
|
||||||
for="email"
|
|
||||||
id="email-label"
|
|
||||||
>
|
|
||||||
Email Address
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
autocomplete="email"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
required=""
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-7 PrivateNotchedOutline-legendNotched-8"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Email Address
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
|
||||||
data-shrink="false"
|
|
||||||
for="password"
|
|
||||||
id="password-label"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required=""
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-7"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Password
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
|
|
||||||
data-shrink="false"
|
|
||||||
for="passwordConfirm"
|
|
||||||
id="passwordConfirm-label"
|
|
||||||
>
|
|
||||||
Confirm Password
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
|
|
||||||
>
|
|
||||||
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-invalid="false"
|
|
||||||
class="MuiInputBase-input MuiOutlinedInput-input"
|
|
||||||
id="passwordConfirm"
|
|
||||||
name="password"
|
|
||||||
required=""
|
|
||||||
type="password"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<fieldset
|
|
||||||
aria-hidden="true"
|
|
||||||
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
|
|
||||||
>
|
|
||||||
<legend
|
|
||||||
class="PrivateNotchedOutline-legendLabelled-7"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Confirm Password
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
|
|
||||||
tabindex="0"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="MuiButton-label"
|
|
||||||
>
|
|
||||||
Sign Up
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="MuiTouchRipple-root"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="MuiGrid-root MuiGrid-item"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Already have an account? Sign In
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import React, { useRef } from 'react';
|
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
|
||||||
import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core';
|
|
||||||
import useStyles from '../Styles';
|
|
||||||
import { useUserContext } from '../Contexts/UserContext';
|
|
||||||
|
|
||||||
export default function SignInForm() {
|
|
||||||
const { signUp, signIn, fetching, setError } = useUserContext();
|
|
||||||
const classes = useStyles();
|
|
||||||
const emailEl = useRef(null);
|
|
||||||
const passwordEl = useRef(null);
|
|
||||||
const confirmEl = useRef(null);
|
|
||||||
const onSubmit = async (event) => {
|
|
||||||
try {
|
|
||||||
event.preventDefault();
|
|
||||||
const email = emailEl.current.value;
|
|
||||||
const password = passwordEl.current.value;
|
|
||||||
const confirm = confirmEl.current.value;
|
|
||||||
await signUp(email, password, confirm);
|
|
||||||
await signIn(email, password);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container component="main" maxWidth="xs">
|
|
||||||
<CssBaseline />
|
|
||||||
<div className={classes.paper}>
|
|
||||||
<Typography component="h1" variant="h5">
|
|
||||||
Sign up
|
|
||||||
</Typography>
|
|
||||||
<form className={classes.form} noValidate onSubmit={onSubmit}>
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
id="email"
|
|
||||||
label="Email Address"
|
|
||||||
name="email"
|
|
||||||
autoComplete="email"
|
|
||||||
autoFocus
|
|
||||||
inputRef={emailEl}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
inputRef={passwordEl}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
name="password"
|
|
||||||
label="Confirm Password"
|
|
||||||
type="password"
|
|
||||||
id="passwordConfirm"
|
|
||||||
inputRef={confirmEl}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
className={classes.submit}
|
|
||||||
disabled={fetching}
|
|
||||||
>
|
|
||||||
{ fetching ? "Signing Up..." : "Sign Up" }
|
|
||||||
</Button>
|
|
||||||
<Grid container>
|
|
||||||
<Grid item>
|
|
||||||
<Link component={RouterLink} to="/" variant="body2">
|
|
||||||
{"Already have an account? Sign In"}
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
paper: {
|
|
||||||
marginTop: theme.spacing(8),
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
margin: theme.spacing(1),
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
width: '100%', // Fix IE 11 issue.
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
submit: {
|
|
||||||
margin: theme.spacing(3, 0, 2),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default useStyles;
|
|
||||||
77
src/components/VehicleAddForm/index.jsx
Normal file
77
src/components/VehicleAddForm/index.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
|
import useStyles from "../useStyles";
|
||||||
|
import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext";
|
||||||
|
import { useStatusContext } from "../Contexts/StatusContext";
|
||||||
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
|
import { Button, TextField, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
|
const MainForm = () => {
|
||||||
|
const { addVehicle, busy } = useVehicleContext();
|
||||||
|
const { setMessage } = useStatusContext();
|
||||||
|
const { token } = useUserContext();
|
||||||
|
const classes = useStyles();
|
||||||
|
const vinEl = useRef(null);
|
||||||
|
|
||||||
|
const onSubmit = async (event) => {
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const {
|
||||||
|
idToken: { jwtToken: authToken },
|
||||||
|
} = token;
|
||||||
|
const formData = {
|
||||||
|
vin: vinEl.current.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await addVehicle(formData, authToken);
|
||||||
|
|
||||||
|
setMessage(`Added ${result.vin}`);
|
||||||
|
vinEl.current.value = "";
|
||||||
|
} catch (e) {
|
||||||
|
setMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.paper}>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Add Vehicle
|
||||||
|
</Typography>
|
||||||
|
<form className={classes.form} noValidate action="{onSubmit}">
|
||||||
|
<TextField
|
||||||
|
id="vin"
|
||||||
|
name="vin"
|
||||||
|
label="VIN"
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
maxLength: "17",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
inputRef={vinEl}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
{busy ? "Submitting..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VehicleAddForm = () => (
|
||||||
|
<VehicleProvider>
|
||||||
|
<MainForm />
|
||||||
|
</VehicleProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default VehicleAddForm;
|
||||||
10
src/components/menuItemStyle.jsx
Normal file
10
src/components/menuItemStyle.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function menuItemStyle(item, selectedItems, theme) {
|
||||||
|
return {
|
||||||
|
fontWeight:
|
||||||
|
selectedItems.indexOf(item) === -1
|
||||||
|
? theme.typography.fontWeightRegular
|
||||||
|
: theme.typography.fontWeightMedium,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default menuItemStyle
|
||||||
110
src/components/useStyles.jsx
Normal file
110
src/components/useStyles.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const MENUITEM_HEIGHT = 48;
|
||||||
|
const MENUITEM_PADDING_TOP = 8;
|
||||||
|
const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
marginTop: theme.spacing(8),
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
width: "100%", // Fix IE 11 issue.
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
margin: theme.spacing(3, 0, 2),
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
formControl: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
width: "100%",
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
chips: {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
chip: {
|
||||||
|
margin: 2,
|
||||||
|
},
|
||||||
|
menuProps: {
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: MENUITEM_HEIGHT * 4.5 + MENUITEM_PADDING_TOP,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
previewChip: {
|
||||||
|
minWidth: 160,
|
||||||
|
maxWidth: 210,
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
transition: theme.transitions.create(["margin", "width"], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
appBarShift: {
|
||||||
|
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
||||||
|
marginLeft: DRAWER_WIDTH,
|
||||||
|
transition: theme.transitions.create(["margin", "width"], {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
width: DRAWER_WIDTH,
|
||||||
|
},
|
||||||
|
drawerHeader: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
// necessary for content to be below app bar
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
transition: theme.transitions.create("margin", {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
marginLeft: -DRAWER_WIDTH,
|
||||||
|
},
|
||||||
|
contentShift: {
|
||||||
|
transition: theme.transitions.create("margin", {
|
||||||
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
rightToolbar: {
|
||||||
|
marginLeft: "auto",
|
||||||
|
marginRight: -12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useStyles;
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
// import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
|
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL || "https://gw-dev.fiskerdps.com/compute_auth";
|
||||||
|
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL || "";
|
||||||
|
|
||||||
let signInResponse = {};
|
let signInResponse = {};
|
||||||
let signUpResponse = {};
|
|
||||||
let verifyResponse = {};
|
let verifyResponse = {};
|
||||||
|
let refreshResponse = {};
|
||||||
|
|
||||||
const logResponse = (response) => {
|
const logResponse = (response) => {
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
ssoAuthorize: () => `${AUTH_URL}/authorize?redirect=${CALLBACK_URL}`,
|
||||||
|
ssoLogout: () => `${AUTH_URL}/logout?redirect=${CALLBACK_URL}`,
|
||||||
signIn: async (username, password) => logResponse(signInResponse),
|
signIn: async (username, password) => logResponse(signInResponse),
|
||||||
signUp: async (username, password) => logResponse(signUpResponse),
|
verify: async (idToken) => logResponse(verifyResponse),
|
||||||
verify: async (accessToken) => logResponse(verifyResponse),
|
refresh: async (refreshToken) => logResponse(refreshResponse),
|
||||||
setSignInResponse: (value) => { signInResponse = value; },
|
setSignInResponse: (value) => { signInResponse = value; },
|
||||||
setSignUpResponse: (value) => { signUpResponse = value; },
|
|
||||||
setVerifyResponse: (value) => { verifyResponse = value; },
|
setVerifyResponse: (value) => { verifyResponse = value; },
|
||||||
|
setRefreshResponse: (value) => { refreshResponse = value; },
|
||||||
}
|
}
|
||||||
12
src/services/__mocks__/timer.js
Normal file
12
src/services/__mocks__/timer.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const timer = {
|
||||||
|
start: jest.fn(),
|
||||||
|
stop: jest.fn(),
|
||||||
|
onMessage: jest.fn(),
|
||||||
|
terminate: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimerWorker = () => {
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getTimerWorker;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import delay from "../../utils/delay";
|
import delay from "../../utils/delay";
|
||||||
|
|
||||||
let uploadFileResponse = { url: "CLOUDFRONT_URL" };
|
let uploadFileResponse = { data: { link: "CLOUDFRONT_URL" } };
|
||||||
let uploadFileDelay = false;
|
let uploadFileDelay = false;
|
||||||
let issuedCancelToken = null;
|
let issuedCancelToken = null;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export const getCancelToken = () => {
|
|||||||
return issuedCancelToken;
|
return issuedCancelToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadFile = async (file, onProgress, cancelToken) => {
|
export const uploadFile = async (file, data, token, onProgress, cancelToken) => {
|
||||||
if (!uploadFileDelay) return uploadFileResponse;
|
if (!uploadFileDelay) return uploadFileResponse;
|
||||||
onProgress(50);
|
onProgress(50);
|
||||||
await delay(10000);
|
await delay(10000);
|
||||||
|
|||||||
20
src/services/__mocks__/vehicles.js
Normal file
20
src/services/__mocks__/vehicles.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
const data = [
|
||||||
|
{ vin: "3C4PDCBG0ET127145" },
|
||||||
|
{ vin: "1G1FP87S3GN100062" },
|
||||||
|
{ vin: "1HGCG325XYA062256" },
|
||||||
|
{ vin: "1J4GZ78YXWC160024" },
|
||||||
|
{ vin: "2C3CCAAG8CH222800" },
|
||||||
|
{ vin: "KNADM4A39C6028108" },
|
||||||
|
{ vin: "1G11C5SL9FF153507" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const vehiclesAPI = {
|
||||||
|
getVehicles: async (search, token) => { return { data: { data } }; },
|
||||||
|
addVehicle: async (vehicle, token) => {
|
||||||
|
data.push(vehicle);
|
||||||
|
return vehicle;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default vehiclesAPI;
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
|
import { fetchRespHandler } from "../utils/http";
|
||||||
|
|
||||||
|
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL || "https://gw-dev.fiskerdps.com/compute_auth";
|
||||||
|
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL || "https://dev-ota-admin.fiskerdps.com";
|
||||||
|
|
||||||
const auth = {
|
const auth = {
|
||||||
signIn: (username, password) => fetch(`${AUTH_URL}/auth/login`, {
|
ssoAuthorize: () => `${AUTH_URL}/authorize?redirect=${CALLBACK_URL}`,
|
||||||
|
ssoLogout: () => `${AUTH_URL}/logout?redirect=${CALLBACK_URL}`,
|
||||||
|
signIn: (code) => fetch(`${AUTH_URL}/token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username,
|
code,
|
||||||
password,
|
redirect: CALLBACK_URL,
|
||||||
})
|
})
|
||||||
}).then((response) => response.json()),
|
}).then(fetchRespHandler),
|
||||||
|
|
||||||
signUp: (username, password) => fetch(`${AUTH_URL}/auth/register`, {
|
verify: (idToken) => fetch(`${AUTH_URL}/verify`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ token: idToken })
|
||||||
username,
|
}).then(fetchRespHandler),
|
||||||
password,
|
|
||||||
})
|
|
||||||
}).then((response) => response.json()),
|
|
||||||
|
|
||||||
verify: (accessToken) => fetch(`${AUTH_URL}/auth/verify`, {
|
refresh: (refreshToken) => fetch(`${AUTH_URL}/refresh`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: accessToken })
|
body: JSON.stringify({ refresh_token: refreshToken })
|
||||||
}).then((response) => response.json()),
|
}).then(fetchRespHandler),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default auth;
|
export default auth;
|
||||||
|
|||||||
23
src/services/auth.test.js
Normal file
23
src/services/auth.test.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import auth from "./auth";
|
||||||
|
|
||||||
|
const testAuthURL = (url, endpoint) => {
|
||||||
|
const u = new URL(url);
|
||||||
|
const path = u.pathname.split("/");
|
||||||
|
|
||||||
|
expect(u.protocol).toMatch(/^http/);
|
||||||
|
expect(u.host).toBeTruthy();
|
||||||
|
expect(path[path.length - 1]).toEqual(endpoint);
|
||||||
|
expect(u.searchParams.get("redirect")).toBeTruthy();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Auth service", () => {
|
||||||
|
describe("Auth URLs", () => {
|
||||||
|
it("Authorize URL", () => {
|
||||||
|
testAuthURL(auth.ssoAuthorize(), "authorize");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Logout URL", () => {
|
||||||
|
testAuthURL(auth.ssoLogout(), "logout");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
41
src/services/timeoutScript.js
Normal file
41
src/services/timeoutScript.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const workerscript = () => {
|
||||||
|
var INTERVAL = 60000;
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
var me = self;
|
||||||
|
var timerID = 0;
|
||||||
|
var deadline;
|
||||||
|
|
||||||
|
function startTimer(duration) {
|
||||||
|
deadline = new Date(Date.now() + duration - INTERVAL);
|
||||||
|
stopTimer();
|
||||||
|
timerID = setInterval(function() {
|
||||||
|
var now = new Date();
|
||||||
|
if (now > deadline) {
|
||||||
|
me.postMessage("timeout");
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
}, INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (timerID > 0) clearInterval(timerID);
|
||||||
|
timerID = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.onmessage = function(e) {
|
||||||
|
if (e.data.action === "start") {
|
||||||
|
startTimer(e.data.duration);
|
||||||
|
}
|
||||||
|
else if (e.data.action === "stop") {
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let code = workerscript.toString();
|
||||||
|
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));
|
||||||
|
|
||||||
|
const blob = new Blob([code], {type: "application/javascript"});
|
||||||
|
const timeout_script = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
module.exports = timeout_script;
|
||||||
40
src/services/timer.js
Normal file
40
src/services/timer.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import worker_script from "./timeoutScript";
|
||||||
|
|
||||||
|
const getTimerWorker = () => {
|
||||||
|
const worker = new Worker(worker_script);
|
||||||
|
let messageHandler = null;
|
||||||
|
const workerHandler = (e) => {
|
||||||
|
if (messageHandler === null) return;
|
||||||
|
messageHandler(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener("message", workerHandler);
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: (duration) => {
|
||||||
|
if (!worker) return;
|
||||||
|
worker.postMessage({
|
||||||
|
action: "start",
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: () => {
|
||||||
|
worker.postMessage({
|
||||||
|
action: "stop",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage: (handler) => {
|
||||||
|
messageHandler = handler;
|
||||||
|
},
|
||||||
|
|
||||||
|
terminate: () => {
|
||||||
|
worker.removeEventListener("message", workerHandler);
|
||||||
|
worker.terminate();
|
||||||
|
messageHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getTimerWorker;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
|
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
export const getCancelToken = () => {
|
export const getCancelToken = () => {
|
||||||
const token = axios.CancelToken;
|
const token = axios.CancelToken;
|
||||||
return token.source();
|
return token.source();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadFile = (file, onProgress, cancelToken) => {
|
export const uploadFile = (file, data, token, onProgress, cancelToken) => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
let options = {
|
let options = {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
"Content-Type": "multipart/form-data",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
cancelToken,
|
cancelToken,
|
||||||
};
|
};
|
||||||
@@ -24,6 +25,9 @@ export const uploadFile = (file, onProgress, cancelToken) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form.append('file', file);
|
for (let key in data) {
|
||||||
return axios.post(UPLOAD_ENDPOINT, form, options);
|
form.append(key, data[key]);
|
||||||
|
}
|
||||||
|
form.append("file", file);
|
||||||
|
return axios.post(`${UPLOAD_ENDPOINT}/upload`, form, options);
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/services/vehicles.js
Normal file
20
src/services/vehicles.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { getAuthHeaderOptions, fetchRespHandler } from "../utils/http"
|
||||||
|
|
||||||
|
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL || "https://gw-dev.fiskerdps.com/ota_update";
|
||||||
|
|
||||||
|
const vehiclesAPI = {
|
||||||
|
addVehicle: async (vehicle, token) => fetch(`${API_ENDPOINT}/vehicle`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
body: JSON.stringify(vehicle),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler),
|
||||||
|
getVehicles: async (search, token) => fetch(`${API_ENDPOINT}/vehicles`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
|
||||||
|
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default vehiclesAPI;
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import '@testing-library/jest-dom';
|
import "@testing-library/jest-dom";
|
||||||
|
|||||||
14
src/utils/http.js
Normal file
14
src/utils/http.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const getAuthHeaderOptions = (token) => ({
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchRespHandler = (response) => {
|
||||||
|
if (response.ok) return response.json();
|
||||||
|
|
||||||
|
return response.text()
|
||||||
|
.then((text) => JSON.parse(text))
|
||||||
|
.catch((e) => ({
|
||||||
|
error: response.statusText,
|
||||||
|
message: `${response.status} ${response.statusText}`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user