frontend/app/index.jsx (386 lines of code) (raw):
import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom";
import { CssBaseline } from "@material-ui/core";
import StorageListComponent from "./StorageComponent.jsx";
import ProjectTypeMultistep from "./multistep/ProjectTypeMultistep.jsx";
import FileEntryList from "./FileEntryList.jsx";
import ProjectEntryList from "./ProjectEntryList/ProjectEntryList.tsx";
import ProjectTypeList from "./ProjectTypeList.jsx";
import FileDeleteComponent from "./delete/FileDeleteComponent.jsx";
import StorageContextComponent from "./delete/StorageDeleteComponent";
import TypeDeleteComponent from "./delete/TypeDeleteComponent.jsx";
import ProjectTemplateIndex from "./ProjectTemplateIndex.jsx";
import ProjectTemplateMultistep from "./multistep/ProjectTemplateMultistep.jsx";
import ProjectTemplateDeleteComponent from "./delete/ProjectTemplateDeleteComponent.jsx";
import ProjectDeleteComponent from "./delete/ProjectEntryDeleteComponent.jsx";
import ProjectEntryEditComponent from "./ProjectEntryList/ProjectEntryEditComponent.tsx";
import PostrunList from "./PostrunList";
import PostrunMultistep from "./multistep/PostrunMultistep.jsx";
import PostrunDeleteComponent from "./delete/PostrunDeleteComponent.jsx";
import ServerDefaults from "./ServerDefaults.jsx";
import axios from "axios";
import { config, library } from "@fortawesome/fontawesome-svg-core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { CoreUserContextProvider } from "./UserContext";
import Raven from "raven-js";
import ProjectValidationView from "./ProjectValidationView.jsx";
import CommissionsList from "./CommissionsList/CommissionsList.tsx";
import ObituariesList from "./ObituariesList/ObituariesList.tsx";
import CommissionMultistepNew from "./multistep/CommissionMultistepNew";
import WorkingGroups from "./WorkingGroups/WorkingGroups.tsx";
import WorkingGroup from "./WorkingGroups/WorkingGroup.tsx";
import {
PlutoThemeProvider,
AppSwitcher,
Header,
JwtDataShape,
OAuthContextData,
OAuthContextProvider,
SystemNotification,
UserContextProvider,
verifyExistingLogin,
handleUnauthorized,
} from "@guardian/pluto-headers";
import "./styles/app.css";
import CommissionEntryEditComponent from "./CommissionsList/CommissionEntryEditComponent";
import ProjectCreateMultistepNew from "./multistep/ProjectCreateMultistepNew";
import StorageMultistepNew from "./multistep/StorageMultistepNew";
import { CssBaseline } from "@material-ui/core";
import ProjectValidationMain from "./ProjectRecordValidation/ProjectValidationMain";
import ValidationJobResults from "./ProjectRecordValidation/ValidationJobResults";
import ProjectBackups from "./ProjectEntryList/ProjectBackups";
import PremiereVersionChange from "./PremiereVersionChange/PremiereVersionChange";
import VersionTranslationsList from "./PremiereVersionTranslation/VersionTranslationsList";
import ProjectDeleteDataComponent from "./ProjectEntryList/ProjectDeleteDataComponent";
import CommissionDeleteDataComponent from "./CommissionsList/CommissionDeleteDataComponent";
import AssetFolderProjectBackups from "./ProjectEntryList/AssetFolderProjectBackups";
import DeletionRecords from "./DeletionRecords/DeletionRecords.tsx";
import DeletionRecord from "./DeletionRecords/DeletionRecord.tsx";
import HelpPage from "./HelpPage/HelpPage.tsx";
library.add(faSearch);
window.React = require("react");
axios.interceptors.request.use(function (config) {
const token = window.localStorage.getItem("pluto:access-token");
if (token) config.headers.Authorization = `Bearer ${token}`;
// this is set in the index.scala.html template file and gives us the value of deployment-root from the server config
// Only apply deployment root when url begins with /api
if (config.url.startsWith("/api")) {
config.baseURL = deploymentRootPath;
}
return config;
});
function parseBool(str) {
return /^true$/i.test(str);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
tokenExpired: false,
currentUsername: "",
isAdmin: false,
loading: false,
plutoConfig: {},
userProfile: undefined,
};
this.onLoggedIn = this.onLoggedIn.bind(this);
this.onLoggedOut = this.onLoggedOut.bind(this);
this.handleUnauthorizedFailed = this.handleUnauthorizedFailed.bind(this);
this.onLoginValid = this.onLoginValid.bind(this);
this.oAuthConfigLoaded = this.oAuthConfigLoaded.bind(this);
axios.interceptors.response.use(
(response) => response,
async (error) => {
handleUnauthorized(
this.state.plutoConfig,
error,
this.handleUnauthorizedFailed
);
return Promise.reject(error);
}
);
axios
.get("/system/publicdsn")
.then((response) => {
Raven.config(response.data.publicDsn).install();
console.log("Sentry initialised for", response.data.publicDsn);
})
.catch((error) => {
console.error("Could not intialise sentry", error);
});
}
handleUnauthorizedFailed() {
// Redirect to login screen
this.setState({
tokenExpired: true,
isLoggedIn: false,
loading: false,
currentUsername: "",
});
}
componentDidMount() {
this.setState({
loading: true,
haveChecked: true,
});
}
async onLoginValid(valid, loginData) {
// Fetch the oauth config
let oAuthConfig;
try {
const response = await fetch("/meta/oauth/config.json");
if (response.status === 200) {
oAuthConfig = await response.json();
this.setState({ plutoConfig: oAuthConfig });
}
} catch (error) {
console.error(error);
}
if (valid) {
return this.setState({
isLoggedIn: true,
loading: false,
currentUsername: loginData
? loginData.preferred_username ?? loginData.username
: "",
isAdmin: loginData
? parseBool(loginData[oAuthConfig.adminClaimName])
: false,
});
}
this.setState({
isLoggedIn: false,
loading: false,
currentUsername: "",
});
}
onLoggedIn(userid, isAdmin) {
console.log("Logged in as", userid);
console.log(`${userid} ${isAdmin ? "is" : "is not"} an admin.`);
this.setState(
{ currentUsername: userid, isAdmin: isAdmin, isLoggedIn: true },
() => {
if (!isAdmin) {
window.location.href = `${deploymentRootPath}/project/?mine`;
}
}
);
}
onLoggedOut() {
this.setState({ currentUsername: "", isLoggedIn: false });
}
componentDidUpdate(prevProps, prevState, snapshot) {
setTimeout(() => {
if (
this.state.haveChecked &&
!this.state.isLoggedIn &&
window.location.pathname !== "/"
) {
console.log("Not logged in, redirecting to pluto-start.");
window.location.assign(
"/refreshLogin?returnTo=" + window.location.pathname
);
}
}, 3000);
}
haveToken() {
return window.localStorage.getItem("pluto:access-token");
}
oAuthConfigLoaded(oAuthConfig) {
//If we already have a user token at mount, verify it and update our internal state.
//If we do not, ignore for the time being; it will be set dynamically when the login occurs.
console.log("Loaded oAuthConfig: ", oAuthConfig);
if (this.haveToken()) {
verifyExistingLogin(oAuthConfig)
.then((profile) => {
const userNameV = this.generateUserName(
profile.preferred_username ?? profile.username
);
this.setState({
userProfile: profile,
isLoggedIn: true,
currentUsername: userNameV,
});
})
.catch((err) => {
console.error("Could not verify existing user profile: ", err);
});
}
}
generateUserName(inputString) {
if (inputString.includes("@")) {
const splitString = inputString.split("@", 1)[0];
const userNameConst = splitString.replace(".", "_");
return userNameConst;
}
return inputString;
}
render() {
return (
<OAuthContextProvider onLoaded={this.oAuthConfigLoaded}>
<UserContextProvider
value={{
profile: this.state.userProfile,
updateProfile: (newValue) =>
this.setState({ userProfile: newValue }),
}}
>
<PlutoThemeProvider>
<CssBaseline />
<CoreUserContextProvider
value={
this.state.isLoggedIn
? {
userName: this.state.currentUsername,
isAdmin: this.state.isAdmin,
}
: null
}
>
<div className="app">
<Header />
<AppSwitcher onLoginValid={this.onLoginValid} />
<div id="mainbody" className="mainbody">
<Switch>
<Route
path="/premversion"
component={VersionTranslationsList}
/>
<Route
path="/storage/:itemid/delete"
component={StorageContextComponent}
/>
<Route
path="/storage/:itemid"
component={StorageMultistepNew}
/>
<Route path="/storage/" component={StorageListComponent} />
<Route
path="/template/:itemid/delete"
component={ProjectTemplateDeleteComponent}
/>
<Route
path="/template/:itemid"
component={ProjectTemplateMultistep}
/>
<Route path="/template/" component={ProjectTemplateIndex} />
<Route
path="/file/changePremiereVersion"
component={PremiereVersionChange}
/>
<Route
path="/file/:itemid/delete"
component={FileDeleteComponent}
/>
<Route path="/file/:itemid" component={FileEntryList} />
<Route path="/file/" component={FileEntryList} />
<Route
path="/type/:itemid/delete"
component={TypeDeleteComponent}
/>
<Route
path="/type/:itemid"
component={ProjectTypeMultistep}
/>
<Route path="/type/" component={ProjectTypeList} />
<Route
path="/project/new"
render={(props) => (
<ProjectCreateMultistepNew
isAdmin={this.state.isAdmin}
{...props}
/>
)}
/>
<Route
path="/project/:itemid/backups"
component={ProjectBackups}
/>
<Route
path="/project/:itemid/assetfolderbackups"
component={AssetFolderProjectBackups}
/>
<Route
path="/project/:itemid/delete"
component={ProjectDeleteComponent}
/>
<Route
path="/project/:itemid/deletedata"
component={ProjectDeleteDataComponent}
/>
<Route
path="/project/:itemid"
component={ProjectEntryEditComponent}
/>
<Route path="/project/" component={ProjectEntryList} />
<Route
path="/commission/new"
render={(props) => (
<CommissionMultistepNew
match={props.match}
userName={this.state.currentUsername}
/>
)}
/>
<Route
path="/commission/:commissionId/deletedata"
component={CommissionDeleteDataComponent}
/>
<Route
path="/commission/:commissionId"
render={(props) => (
<CommissionEntryEditComponent {...props} />
)}
/>
<Route path="/commission/" component={CommissionsList} />
<Route path="/obituaries/" component={ObituariesList} />
<Route
path="/working-group/:itemid"
component={WorkingGroup}
/>
<Route path="/working-group/" component={WorkingGroups} />
<Route
path="/validate/project/:jobId"
component={ValidationJobResults}
/>
<Route
path="/validate/project"
component={ProjectValidationMain}
/>
<Route
path="/postrun/:itemid/delete"
component={PostrunDeleteComponent}
/>
<Route
path="/postrun/:itemid"
component={PostrunMultistep}
/>
<Route path="/postrun/" component={PostrunList} />
<Route path="/defaults/" component={ServerDefaults} />
<Route path="/deleted/:id" component={DeletionRecord} />
<Route path="/deleted/" component={DeletionRecords} />
<Route path="/help/" component={HelpPage} />
<Route
exact
path="/"
render={() => <Redirect to="/project/" />}
/>
</Switch>
</div>
</div>
<SystemNotification />
</CoreUserContextProvider>
</PlutoThemeProvider>
</UserContextProvider>
</OAuthContextProvider>
);
}
}
render(
<BrowserRouter basename={deploymentRootPath}>
<App />
</BrowserRouter>,
document.getElementById("app")
);