Compare commits
13 Commits
89cf1fcd76
...
1e45226205
Author | SHA1 | Date |
---|---|---|
Tudor Stanciu | 1e45226205 | |
Tudor Stanciu | 25a2ff95e4 | |
Tudor Stanciu | 735bba1e4d | |
Tudor Stanciu | 677de87434 | |
Tudor Stanciu | ad254682f0 | |
Tudor Stanciu | 453dfa96ae | |
Tudor Stanciu | 79f9ce0aca | |
Tudor Stanciu | 3153d17e42 | |
Tudor Stanciu | 2b9bc9fb08 | |
Tudor Stanciu | 3029df96a2 | |
Tudor Stanciu | 234760c1f4 | |
Tudor Stanciu | 8eb624f876 | |
Tudor Stanciu | bed8aec44f |
|
@ -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": "^_" }]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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..."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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..."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
useLayoutState,
|
||||
useLayoutDispatch,
|
||||
toggleSidebar
|
||||
} from "../../context/LayoutContext";
|
||||
} from "../../contexts/LayoutContext";
|
||||
|
||||
import menu from "./menu";
|
||||
|
||||
|
|
|
@ -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) {
|
|
@ -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) {
|
|
@ -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) {
|
|
@ -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 = {
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import useServerApi from "./useServerApi";
|
||||
|
||||
export { useServerApi };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
}
|
||||
}));
|
|
@ -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;
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
|
@ -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]
|
|
@ -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;
|
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
After Width: | Height: | Size: 967 KiB |
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export const onTextFieldChange = (onPropertyChange) => (event) =>
|
||||
onPropertyChange(event.target.value);
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in New Issue