You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

431 lines
16 KiB
TypeScript

// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Redirect, Route, Switch } from 'react-router';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Col, Row } from 'antd/lib/grid';
import Layout from 'antd/lib/layout';
import Modal from 'antd/lib/modal';
import notification from 'antd/lib/notification';
import Spin from 'antd/lib/spin';
import Text from 'antd/lib/typography/Text';
import 'antd/dist/antd.css';
import LoginPageContainer from 'containers/login-page/login-page';
import LoginWithTokenComponent from 'components/login-with-token/login-with-token';
import RegisterPageContainer from 'containers/register-page/register-page';
import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page';
import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
import Header from 'components/header/header';
import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary';
import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import ModelsPageContainer from 'containers/models-page/models-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import TaskPageContainer from 'containers/task-page/task-page';
import ProjectsPageComponent from 'components/projects-page/projects-page';
import CreateProjectPageComponent from 'components/create-project-page/create-project-page';
import ProjectPageComponent from 'components/project-page/project-page';
import CloudStoragesPageComponent from 'components/cloud-storages-page/cloud-storages-page';
import CreateCloudStoragePageComponent from 'components/create-cloud-storage-page/create-cloud-storage-page';
import UpdateCloudStoragePageComponent from 'components/update-cloud-storage-page/update-cloud-storage-page';
import OrganizationPage from 'components/organization-page/organization-page';
import CreateOrganizationComponent from 'components/create-organization-page/create-organization-page';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import getCore from 'cvat-core-wrapper';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { NotificationsState } from 'reducers/interfaces';
import { customWaViewHit } from 'utils/enviroment';
import showPlatformNotification, {
platformInfo,
stopNotifications,
showUnsupportedNotification,
} from 'utils/platform-checker';
import '../styles.scss';
import EmailConfirmationPage from './email-confirmation-page/email-confirmed';
interface CVATAppProps {
loadFormats: () => void;
loadAbout: () => void;
verifyAuthorized: () => void;
loadUserAgreements: () => void;
initPlugins: () => void;
initModels: () => void;
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
switchSettingsDialog: () => void;
loadAuthActions: () => void;
loadOrganizations: () => void;
keyMap: KeyMap;
userInitialized: boolean;
userFetching: boolean;
organizationsFetching: boolean;
organizationsInitialized: boolean;
pluginsInitialized: boolean;
pluginsFetching: boolean;
modelsInitialized: boolean;
modelsFetching: boolean;
formatsInitialized: boolean;
formatsFetching: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
userAgreementsFetching: boolean;
userAgreementsInitialized: boolean;
authActionsFetching: boolean;
authActionsInitialized: boolean;
notifications: NotificationsState;
user: any;
isModelPluginActive: boolean;
}
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void {
const core = getCore();
const { verifyAuthorized, history, location } = this.props;
// configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
// Logger configuration
const userActivityCallback: (() => void)[] = [];
window.addEventListener('click', () => {
userActivityCallback.forEach((handler) => handler());
});
core.logger.configure(() => window.document.hasFocus, userActivityCallback);
customWaViewHit(location.pathname, location.search, location.hash);
history.listen((_location) => {
customWaViewHit(_location.pathname, _location.search, _location.hash);
});
verifyAuthorized();
const {
name, version, engine, os,
} = platformInfo();
if (showPlatformNotification()) {
stopNotifications(false);
Modal.warning({
title: 'Unsupported platform detected',
className: 'cvat-modal-unsupported-platform-warning',
content: (
<>
<Row>
<Col>
<Text>
{`The browser you are using is ${name} ${version} based on ${engine}.` +
' CVAT was tested in the latest versions of Chrome and Firefox.' +
' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
</Col>
</Row>
<Row>
<Col>
<Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col>
</Row>
</>
),
onOk: () => stopNotifications(true),
});
} else if (showUnsupportedNotification()) {
stopNotifications(false);
Modal.warning({
title: 'Unsupported features detected',
className: 'cvat-modal-unsupported-features-warning',
content: (
<Text>
{`${name} v${version} does not support API, which is used by CVAT. `}
It is strongly recommended to update your browser.
</Text>
),
onOk: () => stopNotifications(true),
});
}
}
public componentDidUpdate(): void {
const {
verifyAuthorized,
loadFormats,
loadAbout,
loadUserAgreements,
initPlugins,
initModels,
loadOrganizations,
loadAuthActions,
userInitialized,
userFetching,
organizationsFetching,
organizationsInitialized,
formatsInitialized,
formatsFetching,
aboutInitialized,
aboutFetching,
pluginsInitialized,
pluginsFetching,
modelsInitialized,
modelsFetching,
user,
userAgreementsFetching,
userAgreementsInitialized,
authActionsFetching,
authActionsInitialized,
isModelPluginActive,
} = this.props;
this.showErrors();
this.showMessages();
if (!userInitialized && !userFetching) {
verifyAuthorized();
return;
}
if (!userAgreementsInitialized && !userAgreementsFetching) {
loadUserAgreements();
return;
}
if (!authActionsInitialized && !authActionsFetching) {
loadAuthActions();
}
if (user == null || !user.isVerified) {
return;
}
if (!organizationsInitialized && !organizationsFetching) {
loadOrganizations();
}
if (!formatsInitialized && !formatsFetching) {
loadFormats();
}
if (!aboutInitialized && !aboutFetching) {
loadAbout();
}
if (isModelPluginActive && !modelsInitialized && !modelsFetching) {
initModels();
}
if (!pluginsInitialized && !pluginsFetching) {
initPlugins();
}
}
private showMessages(): void {
function showMessage(title: string): void {
notification.info({
message: (
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
});
}
const { notifications, resetMessages } = this.props;
let shown = false;
for (const where of Object.keys(notifications.messages)) {
for (const what of Object.keys((notifications as any).messages[where])) {
const message = (notifications as any).messages[where][what];
shown = shown || !!message;
if (message) {
showMessage(message);
}
}
}
if (shown) {
resetMessages();
}
}
private showErrors(): void {
function showError(title: string, _error: any, className?: string): void {
const error = _error.toString();
const dynamicProps = typeof className === 'undefined' ? {} : { className };
notification.error({
...dynamicProps,
message: (
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
description: error.length > 200 ? 'Open the Browser Console to get details' : error,
});
// eslint-disable-next-line no-console
console.error(error);
}
const { notifications, resetErrors } = this.props;
let shown = false;
for (const where of Object.keys(notifications.errors)) {
for (const what of Object.keys((notifications as any).errors[where])) {
const error = (notifications as any).errors[where][what];
shown = shown || !!error;
if (error) {
showError(error.message, error.reason, error.className);
}
}
}
if (shown) {
resetErrors();
}
}
// Where you go depends on your URL
public render(): JSX.Element {
const {
userInitialized,
aboutInitialized,
pluginsInitialized,
formatsInitialized,
modelsInitialized,
organizationsInitialized,
switchShortcutsDialog,
switchSettingsDialog,
user,
keyMap,
location,
isModelPluginActive,
} = this.props;
const readyForRender =
(userInitialized && (user == null || !user.isVerified)) ||
(userInitialized &&
formatsInitialized &&
pluginsInitialized &&
aboutInitialized &&
organizationsInitialized &&
(!isModelPluginActive || modelsInitialized));
const subKeyMap = {
SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS,
SWITCH_SETTINGS: keyMap.SWITCH_SETTINGS,
};
const handlers = {
SWITCH_SHORTCUTS: (event: KeyboardEvent) => {
if (event) event.preventDefault();
switchShortcutsDialog();
},
SWITCH_SETTINGS: (event: KeyboardEvent) => {
if (event) event.preventDefault();
switchSettingsDialog();
},
};
if (readyForRender) {
if (user && user.isVerified) {
return (
<GlobalErrorBoundary>
<Layout>
<Header />
<Layout.Content style={{ height: '100%' }}>
<ShortcutsDialog />
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers}>
<Switch>
<Route exact path='/projects' component={ProjectsPageComponent} />
<Route exact path='/projects/create' component={CreateProjectPageComponent} />
<Route exact path='/projects/:id' component={ProjectPageComponent} />
<Route exact path='/tasks' component={TasksPageContainer} />
<Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} />
<Route exact path='/cloudstorages' component={CloudStoragesPageComponent} />
<Route
exact
path='/cloudstorages/create'
component={CreateCloudStoragePageComponent}
/>
<Route
exact
path='/cloudstorages/update/:id'
component={UpdateCloudStoragePageComponent}
/>
<Route
exact
path='/organizations/create'
component={CreateOrganizationComponent}
/>
<Route exact path='/organization' component={OrganizationPage} />
{isModelPluginActive && (
<Route exact path='/models' component={ModelsPageContainer} />
)}
<Redirect
push
to={new URLSearchParams(location.search).get('next') || '/tasks'}
/>
</Switch>
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<ExportDatasetModal />
{/* eslint-disable-next-line */}
<a id='downloadAnchor' target='_blank' style={{ display: 'none' }} download />
</Layout.Content>
</Layout>
</GlobalErrorBoundary>
);
}
return (
<GlobalErrorBoundary>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Route
exact
path='/auth/login-with-token/:sessionId/:token'
component={LoginWithTokenComponent}
/>
<Route exact path='/auth/password/reset' component={ResetPasswordPageComponent} />
<Route
exact
path='/auth/password/reset/confirm'
component={ResetPasswordPageConfirmComponent}
/>
<Route exact path='/auth/email-confirmation' component={EmailConfirmationPage} />
<Redirect
to={location.pathname.length > 1 ? `/auth/login/?next=${location.pathname}` : '/auth/login'}
/>
</Switch>
</GlobalErrorBoundary>
);
}
return <Spin size='large' className='cvat-spinner' />;
}
}
export default withRouter(CVATApplication);