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": {
|
"@babel/generator": {
|
||||||
"version": "7.14.5",
|
"version": "7.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.4.0.tgz",
|
||||||
"integrity": "sha512-OUH9RhfDJPhybQL3owwrSDIXz2yVKXg5lYeOZjyRCiT9wqywNK0FeYyDByOwNIZnnIQoQYmuSrMv+pOX0Uqkmw=="
|
"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": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"@material-ui/styles": "^4.11.3",
|
"@material-ui/styles": "^4.11.3",
|
||||||
"@mdi/js": "^5.9.55",
|
"@mdi/js": "^5.9.55",
|
||||||
"@mdi/react": "^1.4.0",
|
"@mdi/react": "^1.4.0",
|
||||||
|
"@babel/eslint-parser": "^7.16.5",
|
||||||
"apexcharts": "^3.24.0",
|
"apexcharts": "^3.24.0",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"i18next-http-backend": "^1.0.10",
|
"i18next-http-backend": "^1.0.10",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mui-datatables": "^3.7.4",
|
"mui-datatables": "^3.7.4",
|
||||||
|
"prop-types": "15.7.2",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-apexcharts": "^1.3.7",
|
"react-apexcharts": "^1.3.7",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
|
|
|
@ -41,7 +41,11 @@
|
||||||
},
|
},
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"More": "More",
|
"More": "More",
|
||||||
"OpenInNewTab": "Open in new tab"
|
"OpenInNewTab": "Open in new tab",
|
||||||
|
"Delete": "Delete",
|
||||||
|
"Deleting": "Deleting",
|
||||||
|
"Save": "Save",
|
||||||
|
"Saving": "Saving"
|
||||||
},
|
},
|
||||||
"Menu": {
|
"Menu": {
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
|
@ -74,6 +78,10 @@
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Category": "Category",
|
"Category": "Category",
|
||||||
"Secured": "Secured",
|
"Secured": "Secured",
|
||||||
|
"PathOnDisk": "Path on disk",
|
||||||
|
"MimeType": "MIME type",
|
||||||
|
"AutomaticMimeType": "Automatic MIME type",
|
||||||
|
"Description": "Description",
|
||||||
"List": {
|
"List": {
|
||||||
"Title": "Resource management",
|
"Title": "Resource management",
|
||||||
"SubTitle": "Resources",
|
"SubTitle": "Resources",
|
||||||
|
@ -86,6 +94,16 @@
|
||||||
"CopyUrl": "Copy resource URL",
|
"CopyUrl": "Copy resource URL",
|
||||||
"LinkCopiedToClipboard": "The link has been copied to the clipboard."
|
"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ă",
|
"Edit": "Editează",
|
||||||
"More": "Mai mult",
|
"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": {
|
"Menu": {
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
|
@ -65,6 +69,10 @@
|
||||||
"Name": "Nume",
|
"Name": "Nume",
|
||||||
"Category": "Categorie",
|
"Category": "Categorie",
|
||||||
"Secured": "Securizat",
|
"Secured": "Securizat",
|
||||||
|
"PathOnDisk": "Cale pe disc",
|
||||||
|
"MimeType": "Tip MIME",
|
||||||
|
"AutomaticMimeType": "Tip MIME automat",
|
||||||
|
"Description": "Descriere",
|
||||||
"List": {
|
"List": {
|
||||||
"Title": "Managementul resurselor",
|
"Title": "Managementul resurselor",
|
||||||
"SubTitle": "Resurse",
|
"SubTitle": "Resurse",
|
||||||
|
@ -77,6 +85,16 @@
|
||||||
"CopyUrl": "Copiați adresa URL a resursei",
|
"CopyUrl": "Copiați adresa URL a resursei",
|
||||||
"LinkCopiedToClipboard": "Linkul a fost copiat în clipboard."
|
"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
|
// pages
|
||||||
import Error from "../pages/error";
|
import Error from "../pages/error";
|
||||||
import Login from "../pages/login";
|
import Login from "../pages/login";
|
||||||
|
import ServerNotAvailable from "../features/server/availability/components/ServerNotAvailable";
|
||||||
|
|
||||||
// context
|
// context
|
||||||
import { useUserState } from "../context/UserContext";
|
import { useUserState } from "../contexts/UserContext";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../contexts/ToastContext";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
@ -27,6 +28,10 @@ export default function App() {
|
||||||
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
|
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
|
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
|
||||||
|
<PrivateRoute
|
||||||
|
path="/server-not-available"
|
||||||
|
component={ServerNotAvailable}
|
||||||
|
/>
|
||||||
<PublicRoute path="/login" component={Login} />
|
<PublicRoute path="/login" component={Login} />
|
||||||
<PrivateRoute path="/" component={Layout} />
|
<PrivateRoute path="/" component={Layout} />
|
||||||
<Route component={Error} />
|
<Route component={Error} />
|
||||||
|
|
|
@ -32,8 +32,8 @@ import {
|
||||||
useLayoutState,
|
useLayoutState,
|
||||||
useLayoutDispatch,
|
useLayoutDispatch,
|
||||||
toggleSidebar
|
toggleSidebar
|
||||||
} from "../../context/LayoutContext";
|
} from "../../contexts/LayoutContext";
|
||||||
import { useUserDispatch, signOut } from "../../context/UserContext";
|
import { useUserDispatch, signOut } from "../../contexts/UserContext";
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,10 +14,11 @@ import AppearancePage from "../../pages/settings/appearance/components/Appearanc
|
||||||
import ContentFooter from "./ContentFooter";
|
import ContentFooter from "./ContentFooter";
|
||||||
|
|
||||||
// containers
|
// 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
|
// context
|
||||||
import { useLayoutState } from "../../context/LayoutContext";
|
import { useLayoutState } from "../../contexts/LayoutContext";
|
||||||
|
|
||||||
// styles
|
// styles
|
||||||
import useStyles from "./styles";
|
import useStyles from "./styles";
|
||||||
|
@ -36,6 +37,7 @@ const Content = () => {
|
||||||
<div id="fakeToolbar" className={classes.fakeToolbar} />
|
<div id="fakeToolbar" className={classes.fakeToolbar} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/dashboard" component={Dashboard} />
|
<Route path="/dashboard" component={Dashboard} />
|
||||||
|
<Route path="/resources/:id(\d+|new)" component={ResourceContainer} />
|
||||||
<Route path="/resources" component={ResourcesContainer} />
|
<Route path="/resources" component={ResourcesContainer} />
|
||||||
<Route path="/appearance" component={AppearancePage} />
|
<Route path="/appearance" component={AppearancePage} />
|
||||||
<Route path="/typography" component={Typography} />
|
<Route path="/typography" component={Typography} />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
|
import ServerAvailabilityProvider from "../../features/server/providers/ServerAvailabilityProvider";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import Header from "../Header/Header";
|
import Header from "../Header/Header";
|
||||||
|
@ -13,13 +14,13 @@ function Layout(props) {
|
||||||
var classes = useStyles();
|
var classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<ServerAvailabilityProvider>
|
||||||
<>
|
<div className={classes.root}>
|
||||||
<Header history={props.history} />
|
<Header history={props.history} />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Content />
|
<Content />
|
||||||
</>
|
</div>
|
||||||
</div>
|
</ServerAvailabilityProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
useLayoutState,
|
useLayoutState,
|
||||||
useLayoutDispatch,
|
useLayoutDispatch,
|
||||||
toggleSidebar
|
toggleSidebar
|
||||||
} from "../../context/LayoutContext";
|
} from "../../contexts/LayoutContext";
|
||||||
|
|
||||||
import menu from "./menu";
|
import menu from "./menu";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
var LayoutStateContext = React.createContext();
|
var LayoutStateContext = React.createContext();
|
||||||
var LayoutDispatchContext = React.createContext();
|
var LayoutDispatchContext = React.createContext();
|
||||||
|
@ -26,6 +27,10 @@ function LayoutProvider({ children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LayoutProvider.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
function useLayoutState() {
|
function useLayoutState() {
|
||||||
var context = React.useContext(LayoutStateContext);
|
var context = React.useContext(LayoutStateContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useReducer, useMemo, useCallback } from "react";
|
import React, { useReducer, useMemo, useCallback } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import useStyles from "../components/Toast/styles";
|
import useStyles from "../components/Toast/styles";
|
||||||
import ToastContainer from "../components/Toast/ToastContainer";
|
import ToastContainer from "../components/Toast/ToastContainer";
|
||||||
|
@ -137,6 +138,10 @@ const ToastProvider = ({ children }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ToastProvider.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
const useToast = () => {
|
const useToast = () => {
|
||||||
const context = React.useContext(ToastDispatchContext);
|
const context = React.useContext(ToastDispatchContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { authenticate, invalidate, validateToken } from "../utils/identity";
|
import { authenticate, invalidate, validateToken } from "../utils/identity";
|
||||||
|
|
||||||
var UserStateContext = React.createContext();
|
var UserStateContext = React.createContext();
|
||||||
|
@ -7,7 +8,7 @@ var UserDispatchContext = React.createContext();
|
||||||
function userReducer(state, action) {
|
function userReducer(state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "LOGIN_SUCCESS":
|
case "LOGIN_SUCCESS":
|
||||||
return { ...state, authenticated: true };
|
return { ...state, ...action.payload, authenticated: true };
|
||||||
case "SIGN_OUT_SUCCESS":
|
case "SIGN_OUT_SUCCESS":
|
||||||
return { ...state, authenticated: false };
|
return { ...state, authenticated: false };
|
||||||
case "LOGIN_FAILURE":
|
case "LOGIN_FAILURE":
|
||||||
|
@ -23,8 +24,10 @@ function userReducer(state, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProvider({ children }) {
|
function UserProvider({ children }) {
|
||||||
|
const { valid, token } = validateToken();
|
||||||
var [state, dispatch] = React.useReducer(userReducer, {
|
var [state, dispatch] = React.useReducer(userReducer, {
|
||||||
authenticated: validateToken() === true
|
authenticated: valid === true,
|
||||||
|
token
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -36,6 +39,10 @@ function UserProvider({ children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserProvider.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
function useUserState() {
|
function useUserState() {
|
||||||
var context = React.useContext(UserStateContext);
|
var context = React.useContext(UserStateContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import useHttpRequest from "./useHttpRequest";
|
import useHttpRequest from "../../../hooks/useHttpRequest";
|
||||||
import { get } from "../utils/axios";
|
import { get } from "../../../utils/axios";
|
||||||
|
|
||||||
const cdn = process.env.REACT_APP_CDN_URL;
|
const cdn = process.env.REACT_APP_CDN_URL;
|
||||||
const endpoints = {
|
const endpoints = {
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import useHttpRequest from "./useHttpRequest";
|
import useHttpRequest from "../../../hooks/useHttpRequest";
|
||||||
import { get } from "../utils/axios";
|
import { get, del } from "../../../utils/axios";
|
||||||
import { defaultResourcesFilters } from "../constants/resourcesConstants";
|
import { defaultResourcesFilters } from "../../../constants/resourcesConstants";
|
||||||
|
|
||||||
const cdn = process.env.REACT_APP_CDN_URL;
|
const cdn = process.env.REACT_APP_CDN_URL;
|
||||||
|
|
||||||
|
@ -41,8 +41,28 @@ const useResourcesApi = () => {
|
||||||
[exec]
|
[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 {
|
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 React, { forwardRef } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { IconButton, Tooltip } from "@material-ui/core";
|
import { IconButton, Tooltip } from "@material-ui/core";
|
||||||
|
|
||||||
const ActionButton = forwardRef((props, _ref) => {
|
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;
|
export default ActionButton;
|
|
@ -1,19 +1,27 @@
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import { Checkbox, FormLabel } from "@material-ui/core";
|
import { Checkbox, FormLabel } from "@material-ui/core";
|
||||||
import MUIDataTable, { debounceSearchRender } from "mui-datatables";
|
import MUIDataTable, { debounceSearchRender } from "mui-datatables";
|
||||||
import { LoadingText } from "../../../components";
|
import { LoadingText } from "../../../../components";
|
||||||
import PageTitle from "../../../components/PageTitle";
|
import PageTitle from "../../../../components/PageTitle";
|
||||||
import { useResourcesApi, useDictionariesApi } from "../../../api";
|
import { useResourcesApi, useDictionariesApi } from "../../api";
|
||||||
import { defaultResourcesFilters } from "../../../constants/resourcesConstants";
|
import { defaultResourcesFilters } from "../../../../constants/resourcesConstants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ActionButton from "./ActionButton";
|
import ActionButton from "./ActionButton";
|
||||||
import SecondaryActionsGroup from "./SecondaryActionsGroup";
|
import SecondaryActionsGroup from "./SecondaryActionsGroup";
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FileCopyOutlined,
|
FileCopyOutlined,
|
||||||
OpenInNewOutlined
|
OpenInNewOutlined,
|
||||||
|
CloudDownloadOutlined,
|
||||||
|
DeleteOutlined
|
||||||
} from "@material-ui/icons";
|
} 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 __ROWS_PER_PAGE_OPTIONS = [10, 20, 50, 100];
|
||||||
const __RESOURCE_NAME_MAX_LENGTH = 35;
|
const __RESOURCE_NAME_MAX_LENGTH = 35;
|
||||||
|
@ -32,6 +40,15 @@ const ResourcesContainer = () => {
|
||||||
const { info } = useToast();
|
const { info } = useToast();
|
||||||
const { getResources } = useResourcesApi();
|
const { getResources } = useResourcesApi();
|
||||||
const { getResourceCategories } = useDictionariesApi();
|
const { getResourceCategories } = useDictionariesApi();
|
||||||
|
const { secureUrl } = useResourceSecurity();
|
||||||
|
const { download } = useFileDownload();
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
component: ResourceDeleteDialog,
|
||||||
|
handleOpen: handleOpenDeleteDialog
|
||||||
|
} = useResourceDeleteDialog(null, null, () =>
|
||||||
|
changeFilters(defaultResourcesFilters)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getResourceCategories().then((r) => setResourceCategories(r));
|
getResourceCategories().then((r) => setResourceCategories(r));
|
||||||
|
@ -69,11 +86,27 @@ const ResourcesContainer = () => {
|
||||||
[resourceCategories]
|
[resourceCategories]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const prepareUrl = useCallback(
|
||||||
|
(url, download, secure) => {
|
||||||
|
if (download) {
|
||||||
|
url = `${url}?download=${download}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secure) {
|
||||||
|
url = secureUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
[secureUrl]
|
||||||
|
);
|
||||||
|
|
||||||
const actions = useMemo(
|
const actions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
code: "edit",
|
code: "edit",
|
||||||
effect: () => alert("edit"),
|
effect: (_event, resource) =>
|
||||||
|
history.push(`/resources/${resource.resourceId}`),
|
||||||
icon: EditOutlined,
|
icon: EditOutlined,
|
||||||
tooltip: t("Generic.Edit"),
|
tooltip: t("Generic.Edit"),
|
||||||
top: true
|
top: true
|
||||||
|
@ -81,7 +114,8 @@ const ResourcesContainer = () => {
|
||||||
{
|
{
|
||||||
code: "copy-url",
|
code: "copy-url",
|
||||||
effect: (_event, resource) => {
|
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"));
|
info(t("Resource.List.Actions.LinkCopiedToClipboard"));
|
||||||
},
|
},
|
||||||
icon: FileCopyOutlined,
|
icon: FileCopyOutlined,
|
||||||
|
@ -91,15 +125,37 @@ const ResourcesContainer = () => {
|
||||||
{
|
{
|
||||||
code: "open-in-new-tab",
|
code: "open-in-new-tab",
|
||||||
effect: (event, resource) => {
|
effect: (event, resource) => {
|
||||||
window.open(resource.url, "_blank");
|
const url = prepareUrl(resource.url, false, resource.secured);
|
||||||
|
window.open(url, "_blank");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
icon: OpenInNewOutlined,
|
icon: OpenInNewOutlined,
|
||||||
tooltip: t("Generic.OpenInNewTab"),
|
tooltip: t("Generic.OpenInNewTab"),
|
||||||
top: false
|
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(
|
const columns = useMemo(
|
||||||
|
@ -320,7 +376,6 @@ const ResourcesContainer = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={t("Resource.List.Title")} />
|
<PageTitle title={t("Resource.List.Title")} />
|
||||||
|
|
||||||
<MUIDataTable
|
<MUIDataTable
|
||||||
title={t("Resource.List.SubTitle")}
|
title={t("Resource.List.SubTitle")}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
@ -386,6 +441,7 @@ const ResourcesContainer = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{ResourceDeleteDialog}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
import { Menu } from "@material-ui/core";
|
import { Menu } from "@material-ui/core";
|
||||||
import ActionButton from "./ActionButton";
|
import ActionButton from "./ActionButton";
|
||||||
import { MoreHorizOutlined } from "@material-ui/icons";
|
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;
|
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 { useCallback, useMemo } from "react";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../contexts/ToastContext";
|
||||||
|
|
||||||
const useHttpRequest = () => {
|
const useHttpRequest = () => {
|
||||||
const { error } = useToast();
|
const { error } = useToast();
|
||||||
|
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
(err) => {
|
(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(message);
|
||||||
},
|
},
|
||||||
[error]
|
[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 Themes from "./themes";
|
||||||
import App from "./components/App";
|
import App from "./components/App";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import { LayoutProvider } from "./context/LayoutContext";
|
import { LayoutProvider } from "./contexts/LayoutContext";
|
||||||
import { UserProvider } from "./context/UserContext";
|
import { UserProvider } from "./contexts/UserContext";
|
||||||
import { ToastProvider } from "./context/ToastContext";
|
import { ToastProvider } from "./contexts/ToastContext";
|
||||||
import "./utils/i18n";
|
import "./utils/i18n";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
|
|
@ -20,7 +20,7 @@ import logo from "./logo.svg";
|
||||||
import google from "../../images/google.svg";
|
import google from "../../images/google.svg";
|
||||||
|
|
||||||
// context
|
// context
|
||||||
import { useUserDispatch, loginUser } from "../../context/UserContext";
|
import { useUserDispatch, loginUser } from "../../contexts/UserContext";
|
||||||
|
|
||||||
function Login(props) {
|
function Login(props) {
|
||||||
var classes = useStyles();
|
var classes = useStyles();
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Notification from "../../components/Notification";
|
||||||
import { Typography, Button } from "../../components/Wrappers/Wrappers";
|
import { Typography, Button } from "../../components/Wrappers/Wrappers";
|
||||||
|
|
||||||
// context
|
// context
|
||||||
import { useToast, useToastState } from "../../context/ToastContext";
|
import { useToast, useToastState } from "../../contexts/ToastContext";
|
||||||
|
|
||||||
export default function NotificationsPage(props) {
|
export default function NotificationsPage(props) {
|
||||||
const classes = useStyles();
|
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 validateToken = () => {
|
||||||
const token = getItem(storageKeys.TOKEN);
|
let token = getItem(storageKeys.TOKEN);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return false;
|
return { valid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = new Date(token.validUntil) >= new Date();
|
const valid = new Date(token.validUntil) >= new Date();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
invalidate();
|
invalidate();
|
||||||
|
token = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid;
|
return { valid, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
export { storageKeys, authenticate, invalidate, validateToken };
|
export { storageKeys, authenticate, invalidate, validateToken };
|
||||||
|
|
Loading…
Reference in New Issue