Compare commits

...

13 Commits

Author SHA1 Message Date
Tudor Stanciu 1e45226205 resource links component 2022-12-18 22:50:32 +02:00
Tudor Stanciu 25a2ff95e4 CheckingServerHealth component 2022-12-18 17:49:32 +02:00
Tudor Stanciu 735bba1e4d ServerNotAvailable translation 2022-12-18 17:28:05 +02:00
Tudor Stanciu 677de87434 check server availability 2022-12-18 17:13:49 +02:00
Tudor Stanciu ad254682f0 delete resource from list 2022-12-18 04:29:14 +02:00
Tudor Stanciu 453dfa96ae delete resource 2022-12-18 03:19:48 +02:00
Tudor Stanciu 79f9ce0aca Mime type component 2022-12-15 01:16:25 +02:00
Tudor Stanciu 3153d17e42 resource component 2022-12-15 00:05:32 +02:00
Tudor Stanciu 2b9bc9fb08 Added eslint and prop-types 2022-12-14 19:12:26 +02:00
Tudor Stanciu 3029df96a2 Add 'download' option for resources 2022-12-14 18:05:57 +02:00
Tudor Stanciu 234760c1f4 resources components refactoring 2022-12-13 18:18:00 +02:00
Tudor Stanciu 8eb624f876 resource container 2022-12-12 03:07:08 +02:00
Tudor Stanciu bed8aec44f add token for secured resources 2022-12-12 02:32:47 +02:00
45 changed files with 1444 additions and 43 deletions

38
.eslintrc.json Normal file
View File

@ -0,0 +1,38 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jest/recommended"
],
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false,
"babelOptions": {
"presets": ["@babel/preset-react"]
},
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"plugins": ["react", "react-hooks", "jest"],
"ignorePatterns": ["**/public", "src/components/*", "src/pages/*"],
"rules": {
"indent": 0,
"linebreak-style": 0,
"quotes": 0,
"semi": 0,
"no-console": 0,
"no-debugger": "warn",
"react/display-name": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }]
}
}

341
notes/theme-example.json Normal file
View File

@ -0,0 +1,341 @@
{
"breakpoints":{
"keys":[
"xs",
"sm",
"md",
"lg",
"xl"
],
"values":{
"xs":0,
"sm":600,
"md":960,
"lg":1280,
"xl":1920
}
},
"direction":"ltr",
"mixins":{
"toolbar":{
"minHeight":56,
"@media (min-width:0px) and (orientation: landscape)":{
"minHeight":48
},
"@media (min-width:600px)":{
"minHeight":64
}
}
},
"overrides":{
"MuiBackdrop":{
"root":{
"backgroundColor":"#4A4A4A1A"
}
},
"MuiMenu":{
"paper":{
"boxShadow":"0px 3px 11px 0px #E8EAFC, 0 3px 3px -2px #B2B2B21A, 0 1px 8px 0 #9A9A9A1A"
}
},
"MuiSelect":{
"icon":{
"color":"#B9B9B9"
}
},
"MuiListItem":{
"root":{
"&$selected":{
"backgroundColor":"#F3F5FF !important",
"&:focus":{
"backgroundColor":"#F3F5FF"
}
}
},
"button":{
"&:hover, &:focus":{
"backgroundColor":"#F3F5FF"
}
}
},
"MuiTouchRipple":{
"child":{
"backgroundColor":"white"
}
},
"MuiTableRow":{
"root":{
"height":56
}
},
"MuiTableCell":{
"root":{
"borderBottom":"1px solid rgba(224, 224, 224, .5)",
"paddingLeft":24
},
"head":{
"fontSize":"0.95rem"
},
"body":{
"fontSize":"0.95rem"
}
},
"PrivateSwitchBase":{
"root":{
"marginLeft":10
}
}
},
"palette":{
"common":{
"black":"#000",
"white":"#fff"
},
"type":"light",
"primary":{
"main":"#536DFE",
"light":"#798dfe",
"dark":"#072cfe",
"contrastText":"#fff"
},
"secondary":{
"main":"#FF5C93",
"light":"#ff82ac",
"dark":"#ff0f60",
"contrastText":"#FFFFFF"
},
"error":{
"light":"#e57373",
"main":"#f44336",
"dark":"#d32f2f",
"contrastText":"#fff"
},
"warning":{
"main":"#FFC260",
"light":"#ffd186",
"dark":"#ffa513",
"contrastText":"rgba(0, 0, 0, 0.87)"
},
"info":{
"main":"#9013FE",
"light":"#a239fe",
"dark":"#6801c4",
"contrastText":"#fff"
},
"success":{
"main":"#3CD4A0",
"light":"#5bdbaf",
"dark":"#23a075",
"contrastText":"rgba(0, 0, 0, 0.87)"
},
"grey":{
"50":"#fafafa",
"100":"#f5f5f5",
"200":"#eeeeee",
"300":"#e0e0e0",
"400":"#bdbdbd",
"500":"#9e9e9e",
"600":"#757575",
"700":"#616161",
"800":"#424242",
"900":"#212121",
"A100":"#d5d5d5",
"A200":"#aaaaaa",
"A400":"#303030",
"A700":"#616161"
},
"contrastThreshold":3,
"tonalOffset":0.2,
"text":{
"primary":"#4A4A4A",
"secondary":"#6E6E6E",
"disabled":"rgba(0, 0, 0, 0.38)",
"hint":"#B9B9B9"
},
"divider":"rgba(0, 0, 0, 0.12)",
"background":{
"paper":"#fff",
"default":"#F6F7FF",
"light":"#F3F5FF"
},
"action":{
"active":"rgba(0, 0, 0, 0.54)",
"hover":"rgba(0, 0, 0, 0.04)",
"hoverOpacity":0.04,
"selected":"rgba(0, 0, 0, 0.08)",
"selectedOpacity":0.08,
"disabled":"rgba(0, 0, 0, 0.26)",
"disabledBackground":"rgba(0, 0, 0, 0.12)",
"disabledOpacity":0.38,
"focus":"rgba(0, 0, 0, 0.12)",
"focusOpacity":0.12,
"activatedOpacity":0.12
}
},
"props":{
},
"shadows":[
"none",
"0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px 0px rgba(0,0,0,0.14),0px 1px 3px 0px rgba(0,0,0,0.12)",
"0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)",
"0px 3px 3px -2px rgba(0,0,0,0.2),0px 3px 4px 0px rgba(0,0,0,0.14),0px 1px 8px 0px rgba(0,0,0,0.12)",
"0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)",
"0px 3px 5px -1px rgba(0,0,0,0.2),0px 5px 8px 0px rgba(0,0,0,0.14),0px 1px 14px 0px rgba(0,0,0,0.12)",
"0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)",
"0px 4px 5px -2px rgba(0,0,0,0.2),0px 7px 10px 1px rgba(0,0,0,0.14),0px 2px 16px 1px rgba(0,0,0,0.12)",
"0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)",
"0px 5px 6px -3px rgba(0,0,0,0.2),0px 9px 12px 1px rgba(0,0,0,0.14),0px 3px 16px 2px rgba(0,0,0,0.12)",
"0px 6px 6px -3px rgba(0,0,0,0.2),0px 10px 14px 1px rgba(0,0,0,0.14),0px 4px 18px 3px rgba(0,0,0,0.12)",
"0px 6px 7px -4px rgba(0,0,0,0.2),0px 11px 15px 1px rgba(0,0,0,0.14),0px 4px 20px 3px rgba(0,0,0,0.12)",
"0px 7px 8px -4px rgba(0,0,0,0.2),0px 12px 17px 2px rgba(0,0,0,0.14),0px 5px 22px 4px rgba(0,0,0,0.12)",
"0px 7px 8px -4px rgba(0,0,0,0.2),0px 13px 19px 2px rgba(0,0,0,0.14),0px 5px 24px 4px rgba(0,0,0,0.12)",
"0px 7px 9px -4px rgba(0,0,0,0.2),0px 14px 21px 2px rgba(0,0,0,0.14),0px 5px 26px 4px rgba(0,0,0,0.12)",
"0px 8px 9px -5px rgba(0,0,0,0.2),0px 15px 22px 2px rgba(0,0,0,0.14),0px 6px 28px 5px rgba(0,0,0,0.12)",
"0px 8px 10px -5px rgba(0,0,0,0.2),0px 16px 24px 2px rgba(0,0,0,0.14),0px 6px 30px 5px rgba(0,0,0,0.12)",
"0px 8px 11px -5px rgba(0,0,0,0.2),0px 17px 26px 2px rgba(0,0,0,0.14),0px 6px 32px 5px rgba(0,0,0,0.12)",
"0px 9px 11px -5px rgba(0,0,0,0.2),0px 18px 28px 2px rgba(0,0,0,0.14),0px 7px 34px 6px rgba(0,0,0,0.12)",
"0px 9px 12px -6px rgba(0,0,0,0.2),0px 19px 29px 2px rgba(0,0,0,0.14),0px 7px 36px 6px rgba(0,0,0,0.12)",
"0px 10px 13px -6px rgba(0,0,0,0.2),0px 20px 31px 3px rgba(0,0,0,0.14),0px 8px 38px 7px rgba(0,0,0,0.12)",
"0px 10px 13px -6px rgba(0,0,0,0.2),0px 21px 33px 3px rgba(0,0,0,0.14),0px 8px 40px 7px rgba(0,0,0,0.12)",
"0px 10px 14px -6px rgba(0,0,0,0.2),0px 22px 35px 3px rgba(0,0,0,0.14),0px 8px 42px 7px rgba(0,0,0,0.12)",
"0px 11px 14px -7px rgba(0,0,0,0.2),0px 23px 36px 3px rgba(0,0,0,0.14),0px 9px 44px 8px rgba(0,0,0,0.12)",
"0px 11px 15px -7px rgba(0,0,0,0.2),0px 24px 38px 3px rgba(0,0,0,0.14),0px 9px 46px 8px rgba(0,0,0,0.12)"
],
"typography":{
"htmlFontSize":16,
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontSize":14,
"fontWeightLight":300,
"fontWeightRegular":400,
"fontWeightMedium":500,
"fontWeightBold":700,
"h1":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":300,
"fontSize":"3rem",
"lineHeight":1.167,
"letterSpacing":"-0.01562em"
},
"h2":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":300,
"fontSize":"2rem",
"lineHeight":1.2,
"letterSpacing":"-0.00833em"
},
"h3":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"1.64rem",
"lineHeight":1.167,
"letterSpacing":"0em"
},
"h4":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"1.5rem",
"lineHeight":1.235,
"letterSpacing":"0.00735em"
},
"h5":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"1.285rem",
"lineHeight":1.334,
"letterSpacing":"0em"
},
"h6":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":500,
"fontSize":"1.142rem",
"lineHeight":1.6,
"letterSpacing":"0.0075em"
},
"subtitle1":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"1rem",
"lineHeight":1.75,
"letterSpacing":"0.00938em"
},
"subtitle2":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":500,
"fontSize":"0.875rem",
"lineHeight":1.57,
"letterSpacing":"0.00714em"
},
"body1":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"1rem",
"lineHeight":1.5,
"letterSpacing":"0.00938em"
},
"body2":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"0.875rem",
"lineHeight":1.43,
"letterSpacing":"0.01071em"
},
"button":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":500,
"fontSize":"0.875rem",
"lineHeight":1.75,
"letterSpacing":"0.02857em",
"textTransform":"uppercase"
},
"caption":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"0.75rem",
"lineHeight":1.66,
"letterSpacing":"0.03333em"
},
"overline":{
"fontFamily":"\\""Roboto\\"", \\""Helvetica\\"", \\""Arial\\"", sans-serif",
"fontWeight":400,
"fontSize":"0.75rem",
"lineHeight":2.66,
"letterSpacing":"0.08333em",
"textTransform":"uppercase"
}
},
"shape":{
"borderRadius":4
},
"transitions":{
"easing":{
"easeInOut":"cubic-bezier(0.4, 0, 0.2, 1)",
"easeOut":"cubic-bezier(0.0, 0, 0.2, 1)",
"easeIn":"cubic-bezier(0.4, 0, 1, 1)",
"sharp":"cubic-bezier(0.4, 0, 0.6, 1)"
},
"duration":{
"shortest":150,
"shorter":200,
"short":250,
"standard":300,
"complex":375,
"enteringScreen":225,
"leavingScreen":195
}
},
"zIndex":{
"mobileStepper":1000,
"speedDial":1050,
"appBar":1100,
"drawer":1200,
"modal":1300,
"snackbar":1400,
"tooltip":1500
},
"customShadows":{
"widget":"0px 3px 11px 0px #E8EAFC, 0 3px 3px -2px #B2B2B21A, 0 1px 8px 0 #9A9A9A1A",
"widgetDark":"0px 3px 18px 0px #4558A3B3, 0 3px 3px -2px #B2B2B21A, 0 1px 8px 0 #9A9A9A1A",
"widgetWide":"0px 12px 33px 0px #E8EAFC, 0 3px 3px -2px #B2B2B21A, 0 1px 8px 0 #9A9A9A1A"
}
}

25
package-lock.json generated
View File

@ -47,6 +47,23 @@
}
}
},
"@babel/eslint-parser": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz",
"integrity": "sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==",
"requires": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
"eslint-visitor-keys": "^2.1.0",
"semver": "^6.3.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"@babel/generator": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz",
@ -2367,6 +2384,14 @@
"resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.4.0.tgz",
"integrity": "sha512-OUH9RhfDJPhybQL3owwrSDIXz2yVKXg5lYeOZjyRCiT9wqywNK0FeYyDByOwNIZnnIQoQYmuSrMv+pOX0Uqkmw=="
},
"@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
"integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
"requires": {
"eslint-scope": "5.1.1"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -26,6 +26,7 @@
"@material-ui/styles": "^4.11.3",
"@mdi/js": "^5.9.55",
"@mdi/react": "^1.4.0",
"@babel/eslint-parser": "^7.16.5",
"apexcharts": "^3.24.0",
"axios": "^0.19.2",
"classnames": "^2.2.6",
@ -35,6 +36,7 @@
"i18next-http-backend": "^1.0.10",
"moment": "^2.29.4",
"mui-datatables": "^3.7.4",
"prop-types": "15.7.2",
"react": "^16.14.0",
"react-apexcharts": "^1.3.7",
"react-dom": "^16.14.0",

View File

@ -41,7 +41,11 @@
},
"Edit": "Edit",
"More": "More",
"OpenInNewTab": "Open in new tab"
"OpenInNewTab": "Open in new tab",
"Delete": "Delete",
"Deleting": "Deleting",
"Save": "Save",
"Saving": "Saving"
},
"Menu": {
"Dashboard": "Dashboard",
@ -74,6 +78,10 @@
"Name": "Name",
"Category": "Category",
"Secured": "Secured",
"PathOnDisk": "Path on disk",
"MimeType": "MIME type",
"AutomaticMimeType": "Automatic MIME type",
"Description": "Description",
"List": {
"Title": "Resource management",
"SubTitle": "Resources",
@ -86,6 +94,16 @@
"CopyUrl": "Copy resource URL",
"LinkCopiedToClipboard": "The link has been copied to the clipboard."
}
}
},
"Links": {
"Title": "Links",
"SubTitle": "Different forms of the URL at which a resource can be accessed"
}
},
"ServerAvailability": {
"Unavailable": "Oops. Looks like the server is currently unavailable.",
"TryAgain": "Try again in a few seconds",
"Retry": "Retry",
"CheckingServerHealth": "Checking server health..."
}
}

View File

@ -32,7 +32,11 @@
},
"Edit": "Editează",
"More": "Mai mult",
"OpenInNewTab": "Deschide într-un tab nou"
"OpenInNewTab": "Deschide într-un tab nou",
"Delete": "Șterge",
"Deleting": "Se şterge",
"Save": "Salvează",
"Saving": "Se salvează"
},
"Menu": {
"Dashboard": "Dashboard",
@ -65,6 +69,10 @@
"Name": "Nume",
"Category": "Categorie",
"Secured": "Securizat",
"PathOnDisk": "Cale pe disc",
"MimeType": "Tip MIME",
"AutomaticMimeType": "Tip MIME automat",
"Description": "Descriere",
"List": {
"Title": "Managementul resurselor",
"SubTitle": "Resurse",
@ -77,6 +85,16 @@
"CopyUrl": "Copiați adresa URL a resursei",
"LinkCopiedToClipboard": "Linkul a fost copiat în clipboard."
}
}
},
"Links": {
"Title": "Legături",
"SubTitle": "Diferite forme ale URL-ului la care poate fi accesată o resursă"
}
},
"ServerAvailability": {
"Unavailable": "Hopa! Se pare că serverul nu este disponibil momentan.",
"TryAgain": "Încercați din nou în câteva secunde",
"Retry": "Reîncercați",
"CheckingServerHealth": "Se verifică starea serverului..."
}
}

View File

@ -7,10 +7,11 @@ import Layout from "./Layout/Layout";
// pages
import Error from "../pages/error";
import Login from "../pages/login";
import ServerNotAvailable from "../features/server/availability/components/ServerNotAvailable";
// context
import { useUserState } from "../context/UserContext";
import { useToast } from "../context/ToastContext";
import { useUserState } from "../contexts/UserContext";
import { useToast } from "../contexts/ToastContext";
import { useTranslation } from "react-i18next";
export default function App() {
@ -27,6 +28,10 @@ export default function App() {
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
<Switch>
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
<PrivateRoute
path="/server-not-available"
component={ServerNotAvailable}
/>
<PublicRoute path="/login" component={Login} />
<PrivateRoute path="/" component={Layout} />
<Route component={Error} />

View File

@ -32,8 +32,8 @@ import {
useLayoutState,
useLayoutDispatch,
toggleSidebar
} from "../../context/LayoutContext";
import { useUserDispatch, signOut } from "../../context/UserContext";
} from "../../contexts/LayoutContext";
import { useUserDispatch, signOut } from "../../contexts/UserContext";
const messages = [
{

View File

@ -14,10 +14,11 @@ import AppearancePage from "../../pages/settings/appearance/components/Appearanc
import ContentFooter from "./ContentFooter";
// containers
import ResourcesContainer from "../../features/resources/components/ResourcesContainer";
import ResourcesContainer from "../../features/resources/list/components/ResourcesContainer";
import ResourceContainer from "../../features/resources/edit/components/ResourceContainer";
// context
import { useLayoutState } from "../../context/LayoutContext";
import { useLayoutState } from "../../contexts/LayoutContext";
// styles
import useStyles from "./styles";
@ -36,6 +37,7 @@ const Content = () => {
<div id="fakeToolbar" className={classes.fakeToolbar} />
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/resources/:id(\d+|new)" component={ResourceContainer} />
<Route path="/resources" component={ResourcesContainer} />
<Route path="/appearance" component={AppearancePage} />
<Route path="/typography" component={Typography} />

View File

@ -1,5 +1,6 @@
import React from "react";
import { withRouter } from "react-router-dom";
import ServerAvailabilityProvider from "../../features/server/providers/ServerAvailabilityProvider";
// components
import Header from "../Header/Header";
@ -13,13 +14,13 @@ function Layout(props) {
var classes = useStyles();
return (
<ServerAvailabilityProvider>
<div className={classes.root}>
<>
<Header history={props.history} />
<Sidebar />
<Content />
</>
</div>
</ServerAvailabilityProvider>
);
}

View File

@ -16,7 +16,7 @@ import {
useLayoutState,
useLayoutDispatch,
toggleSidebar
} from "../../context/LayoutContext";
} from "../../contexts/LayoutContext";
import menu from "./menu";

View File

@ -1,4 +1,5 @@
import React from "react";
import PropTypes from "prop-types";
var LayoutStateContext = React.createContext();
var LayoutDispatchContext = React.createContext();
@ -26,6 +27,10 @@ function LayoutProvider({ children }) {
);
}
LayoutProvider.propTypes = {
children: PropTypes.node
};
function useLayoutState() {
var context = React.useContext(LayoutStateContext);
if (context === undefined) {

View File

@ -1,4 +1,5 @@
import React, { useReducer, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import { toast } from "react-toastify";
import useStyles from "../components/Toast/styles";
import ToastContainer from "../components/Toast/ToastContainer";
@ -137,6 +138,10 @@ const ToastProvider = ({ children }) => {
);
};
ToastProvider.propTypes = {
children: PropTypes.node
};
const useToast = () => {
const context = React.useContext(ToastDispatchContext);
if (context === undefined) {

View File

@ -1,4 +1,5 @@
import React from "react";
import PropTypes from "prop-types";
import { authenticate, invalidate, validateToken } from "../utils/identity";
var UserStateContext = React.createContext();
@ -7,7 +8,7 @@ var UserDispatchContext = React.createContext();
function userReducer(state, action) {
switch (action.type) {
case "LOGIN_SUCCESS":
return { ...state, authenticated: true };
return { ...state, ...action.payload, authenticated: true };
case "SIGN_OUT_SUCCESS":
return { ...state, authenticated: false };
case "LOGIN_FAILURE":
@ -23,8 +24,10 @@ function userReducer(state, action) {
}
function UserProvider({ children }) {
const { valid, token } = validateToken();
var [state, dispatch] = React.useReducer(userReducer, {
authenticated: validateToken() === true
authenticated: valid === true,
token
});
return (
@ -36,6 +39,10 @@ function UserProvider({ children }) {
);
}
UserProvider.propTypes = {
children: PropTypes.node
};
function useUserState() {
var context = React.useContext(UserStateContext);
if (context === undefined) {

View File

@ -1,6 +1,6 @@
import { useCallback } from "react";
import useHttpRequest from "./useHttpRequest";
import { get } from "../utils/axios";
import useHttpRequest from "../../../hooks/useHttpRequest";
import { get } from "../../../utils/axios";
const cdn = process.env.REACT_APP_CDN_URL;
const endpoints = {

View File

@ -1,7 +1,7 @@
import { useCallback } from "react";
import useHttpRequest from "./useHttpRequest";
import { get } from "../utils/axios";
import { defaultResourcesFilters } from "../constants/resourcesConstants";
import useHttpRequest from "../../../hooks/useHttpRequest";
import { get, del } from "../../../utils/axios";
import { defaultResourcesFilters } from "../../../constants/resourcesConstants";
const cdn = process.env.REACT_APP_CDN_URL;
@ -41,8 +41,28 @@ const useResourcesApi = () => {
[exec]
);
const getResource = useCallback(
(resourceId, options) => {
const endpoint = `${cdn}/admin/resource?ResourceId=${resourceId}`;
const promise = exec(() => get(endpoint), options);
return promise;
},
[exec]
);
const deleteResource = useCallback(
(resourceId, options) => {
const endpoint = `${cdn}/admin/resource?ResourceId=${resourceId}`;
const promise = exec(() => del(endpoint), options);
return promise;
},
[exec]
);
return {
getResources
getResources,
getResource,
deleteResource
};
};

View File

@ -0,0 +1,91 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Card,
CardHeader,
CardContent,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
ListItemIcon,
IconButton,
Link,
Tooltip
} from "@material-ui/core";
import style from "../styles";
import { LinkOutlined, FileCopyOutlined } from "@material-ui/icons";
import { useToast, useResourceSecurity } from "../../../../hooks";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(style);
const LinksComponent = ({ urls, secured }) => {
const classes = useStyles();
const { t } = useTranslation();
const { info } = useToast();
const { secureUrl } = useResourceSecurity();
const handleToggle = (url) => () => {
const urlMustBeSecured = secured || url.includes("id=");
const link = urlMustBeSecured ? secureUrl(url) : url;
navigator.clipboard.writeText(link);
info(t("Resource.List.Actions.LinkCopiedToClipboard"));
};
const preventDefault = (event) => event.preventDefault();
return (
<Card className={classes.linksCard}>
<CardHeader
className={classes.linksHeader}
title={t("Resource.Links.Title")}
subheader={t("Resource.Links.SubTitle")}
/>
<CardContent>
<List>
{urls.map((value, index) => {
return (
<ListItem
key={`url-${index}`}
dense
button
onClick={handleToggle(value)}
>
<ListItemIcon>
<LinkOutlined />
</ListItemIcon>
<ListItemText
primary={
<Link href={value} onClick={preventDefault}>
{value}
</Link>
}
/>
<ListItemSecondaryAction>
<Tooltip title={"Copy"}>
<IconButton
edge="end"
aria-label="comments"
size="small"
onClick={handleToggle(value)}
>
<FileCopyOutlined />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
})}
</List>
</CardContent>
</Card>
);
};
LinksComponent.propTypes = {
urls: PropTypes.array.isRequired,
secured: PropTypes.bool.isRequired
};
export default LinksComponent;

View File

@ -0,0 +1,77 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Grid, TextField, FormControlLabel, Checkbox } from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import { useTranslation } from "react-i18next";
const MimeTypeComponent = ({ mimeType, mimeTypes, onPropertyChange }) => {
const [isAutomaticMimeType, setIsAutomaticMimeType] = useState(
mimeType.mimeTypeId === null
);
const { t } = useTranslation();
const handleChangeAutomaticMimeType = (event) => {
const checked = event.target.checked;
setIsAutomaticMimeType(checked);
onPropertyChange("mimeTypeId")(null);
};
return (
<>
<Grid item xs={12} sm={9}>
{isAutomaticMimeType ? (
<TextField
id="automatic-resource-mime-type"
label={t("Resource.AutomaticMimeType")}
fullWidth
disabled
value={mimeType.mimeTypeName}
/>
) : (
<Autocomplete
id="resource-mime-type"
options={mimeTypes}
value={mimeType.mimeTypeId}
onChange={(_event, value, _reason, _details) =>
onPropertyChange("mimeTypeId")(value.mimeTypeId)
}
getOptionLabel={(option) => {
const optionIsObject =
typeof option === "object" && option !== null;
if (optionIsObject) return option.mimeTypeName;
const mimeType = mimeTypes.find((z) => z.mimeTypeId === option);
return mimeType.mimeTypeName;
}}
getOptionSelected={(option, value) => option.mimeTypeId === value}
renderInput={(params) => (
<TextField {...params} label={t("Resource.MimeType")} />
)}
/>
)}
</Grid>
<Grid item xs={12} sm={3}>
<FormControlLabel
control={
<Checkbox
checked={isAutomaticMimeType}
onChange={handleChangeAutomaticMimeType}
name="automatic-mime-type"
color="primary"
/>
}
label={t("Resource.AutomaticMimeType")}
/>
</Grid>
</>
);
};
MimeTypeComponent.propTypes = {
mimeType: PropTypes.object.isRequired,
mimeTypes: PropTypes.array.isRequired,
onPropertyChange: PropTypes.func.isRequired
};
export default MimeTypeComponent;

View File

@ -0,0 +1,189 @@
import React from "react";
import PropTypes from "prop-types";
import {
Grid,
Paper,
ButtonBase,
TextField,
FormControlLabel,
Checkbox,
Button
} from "@material-ui/core";
import Autocomplete from "@material-ui/lab/Autocomplete";
import { Save as SaveIcon, Delete as DeleteIcon } from "@material-ui/icons";
import { makeStyles } from "@material-ui/core/styles";
import style from "../styles";
import ResourcePreviewComponent from "./ResourcePreviewComponent";
import MimeTypeComponent from "./MimeTypeComponent";
import useResourceDeleteDialog from "../../hooks/useResourceDeleteDialog";
import { onTextFieldChange } from "../../../../utils/adapters";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import LinksComponent from "./LinksComponent";
const useStyles = makeStyles(style);
const ResourceComponent = ({
resource,
mimeTypes,
resourceCategories,
onPropertyChange,
processing,
setProcessing
}) => {
const classes = useStyles();
const history = useHistory();
const { t } = useTranslation();
const {
component: ResourceDeleteDialog,
handleOpen: handleOpenDeleteDialog
} = useResourceDeleteDialog(resource, setProcessing, () =>
history.push("/resources")
);
return (
<>
<Paper className={classes.resourceData}>
<Grid container spacing={2}>
<Grid item xs={12} sm={5}>
<ButtonBase>
<ResourcePreviewComponent resource={resource} />
</ButtonBase>
</Grid>
<Grid item xs={12} sm={7}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
id="resource-code"
label={t("Resource.Code")}
fullWidth
required
value={resource.resourceCode}
onChange={onTextFieldChange(onPropertyChange("resourceCode"))}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
id="resource-name"
label={t("Resource.Name")}
fullWidth
required
value={resource.resourceName}
onChange={onTextFieldChange(onPropertyChange("resourceName"))}
/>
</Grid>
<Grid item xs={12}>
<TextField
id="resource-path"
label={t("Resource.PathOnDisk")}
fullWidth
disabled
value={resource.resourcePath}
/>
</Grid>
<MimeTypeComponent
mimeType={{
mimeTypeId: resource.mimeTypeId,
mimeTypeName: resource.mimeType
}}
mimeTypes={mimeTypes}
onPropertyChange={onPropertyChange}
/>
<Grid item xs={12} sm={9}>
<Autocomplete
id="resource-category"
options={resourceCategories}
value={resource.categoryId}
onChange={(_event, value, _reason, _details) =>
onPropertyChange("categoryId")(value.categoryId)
}
getOptionLabel={(option) => {
const optionIsObject =
typeof option === "object" && option !== null;
if (optionIsObject) return option.categoryName;
const category = resourceCategories.find(
(z) => z.categoryId === option
);
return category.categoryName;
}}
getOptionSelected={(option, value) =>
option.categoryId === value
}
renderInput={(params) => (
<TextField {...params} label={t("Resource.Category")} />
)}
/>
</Grid>
<Grid item xs={12} sm={3}>
<FormControlLabel
control={
<Checkbox
checked={resource.secured}
onChange={(event) =>
onPropertyChange("secured")(event.target.checked)
}
name="resources-is-secured"
color="primary"
/>
}
label={t("Resource.Secured")}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
multiline
rows={12}
id="resource-description"
label={t("Resource.Description")}
variant="outlined"
/>
</Grid>
<Grid container justify="flex-end">
<Grid item>
<Button
variant="outlined"
color="secondary"
className={classes.button}
startIcon={<DeleteIcon />}
onClick={handleOpenDeleteDialog}
disabled={!!processing}
>
{processing === "delete"
? t("Generic.Deleting")
: t("Generic.Delete")}
</Button>
<Button
variant="outlined"
color="primary"
className={classes.button}
startIcon={<SaveIcon />}
disabled={!!processing}
>
{processing === "delete"
? t("Generic.Saving")
: t("Generic.Save")}
</Button>
</Grid>
</Grid>
</Grid>
</Paper>
{ResourceDeleteDialog}
<LinksComponent urls={resource.urls} secured={resource.secured} />
</>
);
};
ResourceComponent.propTypes = {
resource: PropTypes.object.isRequired,
mimeTypes: PropTypes.array.isRequired,
resourceCategories: PropTypes.array.isRequired,
onPropertyChange: PropTypes.func.isRequired,
processing: PropTypes.string,
setProcessing: PropTypes.func.isRequired
};
export default ResourceComponent;

View File

@ -0,0 +1,70 @@
import React, { useState, useMemo, useEffect } from "react";
import PageTitle from "../../../../components/PageTitle";
import { useParams } from "react-router-dom";
import { useResourcesApi, useDictionariesApi } from "../../api";
import { LoadingText } from "../../../../components";
import ResourceComponent from "./ResourceComponent";
import { makeStyles } from "@material-ui/core/styles";
import style from "../styles";
import { useToast } from "../../../../hooks";
const useStyles = makeStyles(style);
const ResourceContainer = () => {
const [state, setState] = useState(null);
const [loading, setLoading] = useState(false);
const [mimeTypes, setMimeTypes] = useState(null);
const [resourceCategories, setResourceCategories] = useState(null);
const [processing, setProcessing] = useState(null);
const classes = useStyles();
const params = useParams();
const { getResource } = useResourcesApi();
const { getMimeTypes, getResourceCategories } = useDictionariesApi();
const { success } = useToast();
const isNew = useMemo(() => params.id === "new", [params.id]);
useEffect(() => {
getMimeTypes().then((r) => setMimeTypes(r));
}, [getMimeTypes]);
useEffect(() => {
getResourceCategories().then((r) => setResourceCategories(r));
}, [getResourceCategories]);
useEffect(() => {
if (isNew) return;
const resourceId = parseInt(params.id);
setLoading(true);
getResource(resourceId).then((resource) => {
setState(resource);
setLoading(false);
});
}, [getResource, isNew, params.id]);
const handlePropertyChange = (prop) => (value) => {
setState((prev) => ({ ...prev, [prop]: value }));
};
if (loading || !state || !mimeTypes || !resourceCategories)
return <LoadingText lines={15} onPaper />;
return (
<>
<PageTitle title={state.resourceName} />
<div className={classes.root}>
<ResourceComponent
resource={state}
mimeTypes={mimeTypes}
resourceCategories={resourceCategories}
onPropertyChange={handlePropertyChange}
processing={processing}
setProcessing={setProcessing}
/>
</div>
</>
);
};
export default ResourceContainer;

View File

@ -0,0 +1,61 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import UnknownResourceType from "../../../../images/UnknownResourceType.jpg";
import style from "../styles";
import { makeStyles } from "@material-ui/core/styles";
import { useResourceSecurity } from "../../../../hooks";
const useStyles = makeStyles(style);
const RESOURCE_TYPES = {
UNKNOWN: "UNKNOWN",
IMAGE: "IMAGE"
};
const getResourceType = (mimeType) => {
if (mimeType.startsWith("image/")) return RESOURCE_TYPES.IMAGE;
return RESOURCE_TYPES.UNKNOWN;
};
const ResourcePreviewComponent = ({ resource }) => {
const classes = useStyles();
const { secureUrl } = useResourceSecurity();
const resourceType = useMemo(() => getResourceType(resource.mimeType), [
resource.mimeType
]);
const resourceUrl = useMemo(
() => (resource.secured ? secureUrl(resource.urls[2]) : resource.urls[2]),
[resource.secured, resource.urls, secureUrl]
);
const isImage = useMemo(() => resourceType === RESOURCE_TYPES.IMAGE, [
resourceType
]);
const isUnknown = useMemo(() => resourceType === RESOURCE_TYPES.UNKNOWN, [
resourceType
]);
return (
<>
{isImage && (
<img src={resourceUrl} alt="..." className={classes.resourceImage} />
)}
{isUnknown && (
<img
src={UnknownResourceType}
alt="..."
className={classes.resourceImage}
/>
)}
</>
);
};
ResourcePreviewComponent.propTypes = {
resource: PropTypes.object.isRequired
};
export default ResourcePreviewComponent;

View File

@ -0,0 +1,28 @@
const style = (theme) => {
return {
root: {
flexGrow: 1
},
resourceData: {
padding: theme.spacing(2),
margin: "auto"
},
linksCard: {
marginTop: theme.spacing(2)
},
linksHeader: {
paddingBottom: 0
},
resourceImage: {
margin: "auto",
display: "block",
maxWidth: "100%",
maxHeight: "100%"
},
button: {
marginRight: theme.spacing(1)
}
};
};
export default style;

View File

@ -0,0 +1,43 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
const DeleteDialog = ({ open, onClose, onConfirm, title }) => {
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{`Are you sure you want to delete the resource '${title}'?`}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
No
</Button>
<Button onClick={onConfirm} color="secondary" autoFocus>
Yes
</Button>
</DialogActions>
</Dialog>
);
};
DeleteDialog.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
title: PropTypes.string
};
export default DeleteDialog;

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import { useResourcesApi } from "../api";
import DeleteDialog from "./components/DeleteDialog";
import { useToast } from "../../../hooks";
const useResourceDeleteDialog = (resource, setProcessing, onComplete) => {
const [state, setState] = useState(
resource ?? { resourceId: null, resourceName: null }
);
const [deleteDialogOpen, setOpen] = useState(false);
const { deleteResource } = useResourcesApi();
const { success } = useToast();
const handleDelete = async (resourceId) => {
setProcessing && setProcessing("delete");
const response = await deleteResource(resourceId);
if (response && response.resourceId === state.resourceId) {
success(`Resource '${state.resourceName}' was successfully deleted.`);
setProcessing && setProcessing(null);
setOpen(false);
onComplete && onComplete(state);
}
};
return {
component: (
<DeleteDialog
title={state.resourceName}
open={deleteDialogOpen}
onClose={() => setOpen(false)}
onConfirm={() => handleDelete(state.resourceId)}
/>
),
handleOpen: (_event, resource) => {
resource && setState(resource);
setOpen(true);
}
};
};
export default useResourceDeleteDialog;

View File

@ -1,4 +1,5 @@
import React, { forwardRef } from "react";
import PropTypes from "prop-types";
import { IconButton, Tooltip } from "@material-ui/core";
const ActionButton = forwardRef((props, _ref) => {
@ -21,4 +22,9 @@ const ActionButton = forwardRef((props, _ref) => {
);
});
ActionButton.propTypes = {
action: PropTypes.object.isRequired,
resource: PropTypes.object.isRequired
};
export default ActionButton;

View File

@ -1,19 +1,27 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Checkbox, FormLabel } from "@material-ui/core";
import MUIDataTable, { debounceSearchRender } from "mui-datatables";
import { LoadingText } from "../../../components";
import PageTitle from "../../../components/PageTitle";
import { useResourcesApi, useDictionariesApi } from "../../../api";
import { defaultResourcesFilters } from "../../../constants/resourcesConstants";
import { LoadingText } from "../../../../components";
import PageTitle from "../../../../components/PageTitle";
import { useResourcesApi, useDictionariesApi } from "../../api";
import { defaultResourcesFilters } from "../../../../constants/resourcesConstants";
import { useTranslation } from "react-i18next";
import ActionButton from "./ActionButton";
import SecondaryActionsGroup from "./SecondaryActionsGroup";
import {
EditOutlined,
FileCopyOutlined,
OpenInNewOutlined
OpenInNewOutlined,
CloudDownloadOutlined,
DeleteOutlined
} from "@material-ui/icons";
import { useToast } from "../../../context/ToastContext";
import {
useToast,
useResourceSecurity,
useFileDownload
} from "../../../../hooks";
import { useHistory } from "react-router-dom";
import useResourceDeleteDialog from "../../hooks/useResourceDeleteDialog";
const __ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100];
const __RESOURCE_NAME_MAX_LENGTH = 35;
@ -32,6 +40,15 @@ const ResourcesContainer = () => {
const { info } = useToast();
const { getResources } = useResourcesApi();
const { getResourceCategories } = useDictionariesApi();
const { secureUrl } = useResourceSecurity();
const { download } = useFileDownload();
const history = useHistory();
const {
component: ResourceDeleteDialog,
handleOpen: handleOpenDeleteDialog
} = useResourceDeleteDialog(null, null, () =>
changeFilters(defaultResourcesFilters)
);
useEffect(() => {
getResourceCategories().then((r) => setResourceCategories(r));
@ -69,11 +86,27 @@ const ResourcesContainer = () => {
[resourceCategories]
);
const prepareUrl = useCallback(
(url, download, secure) => {
if (download) {
url = `${url}?download=${download}`;
}
if (secure) {
url = secureUrl(url);
}
return url;
},
[secureUrl]
);
const actions = useMemo(
() => [
{
code: "edit",
effect: () => alert("edit"),
effect: (_event, resource) =>
history.push(`/resources/${resource.resourceId}`),
icon: EditOutlined,
tooltip: t("Generic.Edit"),
top: true
@ -81,7 +114,8 @@ const ResourcesContainer = () => {
{
code: "copy-url",
effect: (_event, resource) => {
navigator.clipboard.writeText(resource.url);
const url = resource.secured ? secureUrl(resource.url) : resource.url;
navigator.clipboard.writeText(url);
info(t("Resource.List.Actions.LinkCopiedToClipboard"));
},
icon: FileCopyOutlined,
@ -91,15 +125,37 @@ const ResourcesContainer = () => {
{
code: "open-in-new-tab",
effect: (event, resource) => {
window.open(resource.url, "_blank");
const url = prepareUrl(resource.url, false, resource.secured);
window.open(url, "_blank");
event.preventDefault();
},
icon: OpenInNewOutlined,
tooltip: t("Generic.OpenInNewTab"),
top: false
},
{
code: "download",
effect: (event, resource) => {
const url = prepareUrl(resource.url, true, resource.secured);
download(url);
event.preventDefault();
},
icon: CloudDownloadOutlined,
tooltip: t("Generic.Download"),
top: false
},
{
code: "delete",
effect: (event, resource) => {
handleOpenDeleteDialog(event, resource);
event.preventDefault();
},
icon: DeleteOutlined,
tooltip: t("Generic.Delete"),
top: false
}
],
[t, info]
[t, info, secureUrl, history, download, prepareUrl, handleOpenDeleteDialog]
);
const columns = useMemo(
@ -320,7 +376,6 @@ const ResourcesContainer = () => {
return (
<>
<PageTitle title={t("Resource.List.Title")} />
<MUIDataTable
title={t("Resource.List.SubTitle")}
columns={columns}
@ -386,6 +441,7 @@ const ResourcesContainer = () => {
}
}}
/>
{ResourceDeleteDialog}
</>
);
};

View File

@ -1,4 +1,5 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Menu } from "@material-ui/core";
import ActionButton from "./ActionButton";
import { MoreHorizOutlined } from "@material-ui/icons";
@ -40,4 +41,9 @@ const SecondaryActionsGroup = ({ resource, actions }) => {
);
};
SecondaryActionsGroup.propTypes = {
resource: PropTypes.object.isRequired,
actions: PropTypes.array.isRequired
};
export default SecondaryActionsGroup;

View File

@ -0,0 +1,3 @@
import useServerApi from "./useServerApi";
export { useServerApi };

View File

@ -0,0 +1,26 @@
import { useCallback } from "react";
import useHttpRequest from "../../../hooks/useHttpRequest";
import { get } from "../../../utils/axios";
const cdn = process.env.REACT_APP_CDN_URL;
const endpoints = {
ping: `${cdn}/health/ping`
};
const useServerApi = () => {
const { exec } = useHttpRequest();
const checkHealth = useCallback(
(options) => {
const promise = exec(() => get(endpoints.ping), options);
return promise;
},
[exec]
);
return {
checkHealth
};
};
export default useServerApi;

View File

@ -0,0 +1,23 @@
import React from "react";
import useStyles from "../styles";
import { Grid, Typography } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import classnames from "classnames";
const CheckingServerHealth = () => {
const classes = useStyles();
const { t } = useTranslation();
return (
<Grid container className={classnames(classes.container, classes.success)}>
<Typography
variant="h1"
className={classnames(classes.textRow, classes.singleText)}
>
{t("ServerAvailability.CheckingServerHealth")}
</Typography>
</Grid>
);
};
export default CheckingServerHealth;

View File

@ -0,0 +1,51 @@
import React from "react";
import { Grid, Paper, Typography, Button } from "@material-ui/core";
import { Link } from "react-router-dom";
import useStyles from "../styles";
import classnames from "classnames";
import ServerIsDown from "../../../../images/ServerIsDown.gif";
import { useTranslation } from "react-i18next";
const ServerNotAvailable = () => {
const classes = useStyles();
const { t } = useTranslation();
return (
<Grid container className={classnames(classes.container, classes.error)}>
<Paper classes={{ root: classes.paperRoot }}>
<div className={classes.imageFrame}>
<img
className={classes.image}
src={ServerIsDown}
alt="server is down"
/>
</div>
<Typography
variant="h5"
className={classnames(classes.textRow, classes.safetyText)}
>
{t("ServerAvailability.Unavailable")}
</Typography>
<Typography
variant="h6"
color="text"
colorBrightness="secondary"
className={classnames(classes.textRow, classes.safetyText)}
>
{t("ServerAvailability.TryAgain")}
</Typography>
<Button
variant="contained"
component={Link}
to="/"
size="large"
className={classes.button}
>
{t("ServerAvailability.Retry")}
</Button>
</Paper>
</Grid>
);
};
export default ServerNotAvailable;

View File

@ -0,0 +1,58 @@
import { makeStyles } from "@material-ui/styles";
export default makeStyles((theme) => ({
container: {
height: "100vh",
width: "100vw",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
position: "absolute",
top: 0,
left: 0
},
error: {
backgroundColor: theme.palette.error.dark
},
success: {
backgroundColor: theme.palette.success.light
},
imageFrame: {
display: "flex",
alignItems: "center",
marginBottom: theme.spacing(2)
},
image: {
width: 200,
marginRight: theme.spacing(2)
},
paperRoot: {
backgroundColor: theme.palette.common.black,
display: "flex",
flexDirection: "column",
alignItems: "center",
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
maxWidth: 404
},
textRow: {
marginBottom: theme.spacing(4),
textAlign: "center"
},
safetyText: {
fontWeight: 300,
color: theme.palette.text.hint
},
singleText: {
fontWeight: 300,
color: theme.palette.info.main
},
button: {
backgroundColor: theme.palette.error.dark,
textTransform: "none",
fontSize: 22
}
}));

View File

@ -0,0 +1,27 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Redirect } from "react-router-dom";
import { useServerApi } from "../api";
import CheckingServerHealth from "../availability/components/CheckingServerHealth";
const ServerAvailabilityProvider = ({ children }) => {
const [ok, setOk] = useState(null);
const { checkHealth } = useServerApi();
useEffect(() => {
checkHealth({
onCompleted: () => setOk(true),
onError: () => setOk(false)
});
}, [checkHealth]);
if (ok === null) return <CheckingServerHealth />;
if (ok === false) return <Redirect to="/server-not-available" />;
return <>{children}</>;
};
ServerAvailabilityProvider.propTypes = {
children: PropTypes.node
};
export default ServerAvailabilityProvider;

View File

@ -1 +1,6 @@
//export { useAuthorizationToken } from "./useAuthorizationToken";
import { useToast } from "../contexts/ToastContext";
import useHttpRequest from "./useHttpRequest";
import useResourceSecurity from "./useResourceSecurity";
import useFileDownload from "./useFileDownload";
export { useToast, useHttpRequest, useResourceSecurity, useFileDownload };

View File

@ -0,0 +1,14 @@
const useFileDownload = () => {
const download = (uri, name = "") => {
const link = document.createElement("a");
link.setAttribute("download", name);
link.href = uri;
document.body.appendChild(link);
link.click();
link.remove();
};
return { download };
};
export default useFileDownload;

View File

@ -1,12 +1,29 @@
import { useCallback, useMemo } from "react";
import { useToast } from "../context/ToastContext";
import { useToast } from "../contexts/ToastContext";
const useHttpRequest = () => {
const { error } = useToast();
const handleError = useCallback(
(err) => {
const message = `${err.title} ${err.correlationId}`;
let message;
switch (err?.statusCode) {
case 500:
message = "Internal server error.";
break;
case 404:
message = err.serverMessage;
break;
case 401:
message = `Unauthorized: ${err.serverMessage}`;
break;
default:
message = "An unexpected error has occurred.";
}
error(message);
},
[error]

View File

@ -0,0 +1,18 @@
import { useUserState } from "../contexts/UserContext";
import { useCallback } from "react";
const useResourceSecurity = () => {
const { token } = useUserState();
const secureUrl = useCallback(
(url) => {
const separator = url.includes("?") ? "&" : "?";
const securedUrl = `${url}${separator}token=${token.raw}`;
return securedUrl;
},
[token.raw]
);
return { secureUrl };
};
export default useResourceSecurity;

BIN
src/images/ServerIsDown.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 KiB

View File

@ -5,9 +5,9 @@ import { CssBaseline } from "@material-ui/core";
import Themes from "./themes";
import App from "./components/App";
import * as serviceWorker from "./serviceWorker";
import { LayoutProvider } from "./context/LayoutContext";
import { UserProvider } from "./context/UserContext";
import { ToastProvider } from "./context/ToastContext";
import { LayoutProvider } from "./contexts/LayoutContext";
import { UserProvider } from "./contexts/UserContext";
import { ToastProvider } from "./contexts/ToastContext";
import "./utils/i18n";
ReactDOM.render(

View File

@ -20,7 +20,7 @@ import logo from "./logo.svg";
import google from "../../images/google.svg";
// context
import { useUserDispatch, loginUser } from "../../context/UserContext";
import { useUserDispatch, loginUser } from "../../contexts/UserContext";
function Login(props) {
var classes = useStyles();

View File

@ -16,7 +16,7 @@ import Notification from "../../components/Notification";
import { Typography, Button } from "../../components/Wrappers/Wrappers";
// context
import { useToast, useToastState } from "../../context/ToastContext";
import { useToast, useToastState } from "../../contexts/ToastContext";
export default function NotificationsPage(props) {
const classes = useStyles();

View File

@ -0,0 +1,2 @@
export const onTextFieldChange = (onPropertyChange) => (event) =>
onPropertyChange(event.target.value);

View File

@ -33,17 +33,18 @@ const invalidate = () => {
};
const validateToken = () => {
const token = getItem(storageKeys.TOKEN);
let token = getItem(storageKeys.TOKEN);
if (!token) {
return false;
return { valid: false };
}
const valid = new Date(token.validUntil) >= new Date();
if (!valid) {
invalidate();
token = null;
}
return valid;
return { valid, token };
};
export { storageKeys, authenticate, invalidate, validateToken };