diff --git a/app/controllers/api/dashboard_controller.rb b/app/controllers/api/dashboard_controller.rb new file mode 100644 index 0000000..afe2807 --- /dev/null +++ b/app/controllers/api/dashboard_controller.rb @@ -0,0 +1,32 @@ +class Api::DashboardController < Api::BaseController + def heap + render json: { errors: [I18n.t('errors.dashboard_controller.unauthorized')] }, status: :unauthorized and return unless current_user.admin? + + service = Iiif::Server.new + + json = serializer.heap( + service.status.dig(:data) + ) + + render json: json, status: :ok + end + + def status + render json: { errors: [I18n.t('errors.dashboard_controller.unauthorized')] }, status: :unauthorized and return unless current_user.admin? + + service = Iiif::Server.new + + json = serializer.status( + service.health.dig(:data, :color), + service.status.dig(:data) + ) + + render json: json, status: :ok + end + + private + + def serializer + DashboardSerializer.new + end +end \ No newline at end of file diff --git a/app/controllers/api/resources_controller.rb b/app/controllers/api/resources_controller.rb index fe952bf..882a9c5 100644 --- a/app/controllers/api/resources_controller.rb +++ b/app/controllers/api/resources_controller.rb @@ -22,14 +22,13 @@ def clear_cache resource = Resource.find(params[:id]) key = resource.send(params[:attribute])&.key - service = Cantaloupe::Api.new + service = Iiif::Server.new response = service.clear_cache(key) - if response.success? + if response[:success?] render json: {}, status: :ok else - error = response['exception'] || response['message'] || response['errors'] - render json: { errors: [error] }, status: :bad_request + render json: { errors: [response[:errors]] }, status: :bad_request end end diff --git a/app/serializers/dashboard_serializer.rb b/app/serializers/dashboard_serializer.rb new file mode 100644 index 0000000..dc6e76c --- /dev/null +++ b/app/serializers/dashboard_serializer.rb @@ -0,0 +1,26 @@ +class DashboardSerializer + def heap(info) + { + used: info.dig(:vm, :usedHeapBytes), + max: info.dig(:vm, :maxHeapBytes) + } + end + + def status(color, info) + { + application: { + name: I18n.t('dashboard_serializer.application_name'), + version: info.dig(:application, :version) + }, + vm: { + name: info.dig(:vm, :name), + version: info.dig(:vm, :version) + }, + os: { + name: info.dig(:vm, :vendor), + processors: info.dig(:vm, :numProcessors) + }, + status_color: color + } + end +end \ No newline at end of file diff --git a/app/services/cantaloupe/api.rb b/app/services/cantaloupe/api.rb deleted file mode 100644 index 4d3e819..0000000 --- a/app/services/cantaloupe/api.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Cantaloupe - class Api - - def clear_cache(key) - auth = { - username: ENV['CANTALOUPE_API_USERNAME'], - password: ENV['CANTALOUPE_API_PASSWORD'] - } - - body = { - verb: 'PurgeItemFromCache', - identifier: key - } - - HTTParty.post("#{ENV['IIIF_HOST']}/tasks", body: body.to_json, basic_auth: auth) - end - - end -end \ No newline at end of file diff --git a/app/services/iiif/server.rb b/app/services/iiif/server.rb new file mode 100644 index 0000000..17a72c3 --- /dev/null +++ b/app/services/iiif/server.rb @@ -0,0 +1,45 @@ +module Iiif + class Server + + def clear_cache(key) + body = { + verb: 'PurgeItemFromCache', + identifier: key + } + + parse_response HTTParty.post("#{ENV['IIIF_HOST']}/tasks", body: body.to_json, basic_auth: basic_auth) + end + + def health + parse_response HTTParty.get("#{ENV['IIIF_HOST']}/health") + end + + def status + parse_response HTTParty.get("#{ENV['IIIF_HOST']}/status", basic_auth: basic_auth) + end + + private + + def basic_auth + { + username: ENV['CANTALOUPE_API_USERNAME'], + password: ENV['CANTALOUPE_API_PASSWORD'] + } + end + + def parse_response(response) + if response.body.nil? || response.body.empty? + body = '{}' + else + body = response.body + end + + { + success?: response.success?, + data: JSON.parse(body, symbolize_names: true), + errors: response['exception'] || response['message'] || response['errors'] + } + end + + end +end \ No newline at end of file diff --git a/client/package.json b/client/package.json index 97b6a2b..636196f 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-uuid": "^1.0.2", + "recharts": "^2.15.0", "semantic-ui-react": "^2.1.2", "underscore": "^1.13.3" }, diff --git a/client/src/components/AttachmentStatus.js b/client/src/components/AttachmentStatus.js deleted file mode 100644 index 9891bc9..0000000 --- a/client/src/components/AttachmentStatus.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow - -import React, { type ComponentType } from 'react'; -import { Icon } from 'semantic-ui-react'; - -type Props = { - className?: string, - value: boolean -}; - -const AttachmentStatus: ComponentType = (props: Props) => { - if (props.value) { - return ( - - ); - } - - return ( - - ); -}; - -export default AttachmentStatus; diff --git a/client/src/components/AttributeView.js b/client/src/components/AttributeView.js new file mode 100644 index 0000000..df000ae --- /dev/null +++ b/client/src/components/AttributeView.js @@ -0,0 +1,28 @@ +// @flow + +import React, { type ComponentType } from 'react'; +import styles from './AttributeView.module.css'; + +type Props = { + label: string, + value: string +}; + +const AttributeView: ComponentType = (props: Props) => ( +
+
+ { props.label } +
+
+ { props.value } +
+
+); + +export default AttributeView; diff --git a/client/src/components/AttributeView.module.css b/client/src/components/AttributeView.module.css new file mode 100644 index 0000000..bca0840 --- /dev/null +++ b/client/src/components/AttributeView.module.css @@ -0,0 +1,8 @@ +.gridItem .label { + color: rgba(0, 0, 0, .6); + padding-top: 0.25em; +} + +.gridItem .value { + font-weight: bold; +} \ No newline at end of file diff --git a/client/src/components/StatusIcon.js b/client/src/components/StatusIcon.js new file mode 100644 index 0000000..b9fea1f --- /dev/null +++ b/client/src/components/StatusIcon.js @@ -0,0 +1,45 @@ +// @flow + +import React, { type ComponentType } from 'react'; +import { Icon } from 'semantic-ui-react'; + +type Props = { + className?: string, + status: 'positive' | 'warning' | 'negative' +}; + +const StatusIcon: ComponentType = (props: Props) => { + if (props.status === 'positive') { + return ( + + ); + } + + if (props.status === 'warning') { + return ( + + ); + } + + if (props.status === 'negative') { + return ( + + ); + } + + return null; +}; + +export default StatusIcon; diff --git a/client/src/components/Widget.js b/client/src/components/Widget.js new file mode 100644 index 0000000..1f5e2fc --- /dev/null +++ b/client/src/components/Widget.js @@ -0,0 +1,52 @@ +// @flow + +import cx from 'classnames'; +import React, { type ComponentType, type Node } from 'react'; +import { + Button, + Dimmer, + Header, + Loader +} from 'semantic-ui-react'; +import styles from './Widget.module.css'; + +type Props = { + children: Node, + className?: string, + loading: boolean, + onReload?: () => void, + title: string +}; + +const Widget: ComponentType = (props: Props) => ( +
+ + + +
+
+ { props.title } +
+ { props.onReload && ( +
+ { props.children } +
+); + +export default Widget; diff --git a/client/src/components/Widget.module.css b/client/src/components/Widget.module.css new file mode 100644 index 0000000..39e899a --- /dev/null +++ b/client/src/components/Widget.module.css @@ -0,0 +1,16 @@ +.widget > .headerContainer { + display: flex; + justify-content: space-between; +} + +.widget > .headerContainer > .ui.header { + margin-top: 0; +} + +.widget > .headerContainer > .ui.button.reload, +.widget > .headerContainer > .ui.button.reload:active, +.widget > .headerContainer > .ui.button.reload:hover, +.widget > .headerContainer > .ui.button.reload:focus { + background: none !important; + box-shadow: none !important; +} \ No newline at end of file diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index 455b6a9..b30857f 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -58,6 +58,13 @@ "name": "Name" } }, + "HeapSpace": { + "labels": { + "free": "Free", + "used": "Used" + }, + "title": "Heap Space" + }, "Home": { "header": "Welcome to Rails-React Template <1>Edit <1>src/App.js and save to reload." }, @@ -178,6 +185,17 @@ "header": "Password Policy" } }, + "SystemStatus": { + "labels": { + "application": "Application", + "operatingSystem": "Operating system", + "processors": "Processors", + "version": "Version", + "vm": "VM", + "vmVersion": "VM version" + }, + "title": "System Status" + }, "UserModal": { "labels": { "user": "User" diff --git a/client/src/pages/Dashboard.js b/client/src/pages/Dashboard.js index 355245d..a3213d1 100644 --- a/client/src/pages/Dashboard.js +++ b/client/src/pages/Dashboard.js @@ -1,11 +1,37 @@ // @flow import React, { type ComponentType } from 'react'; +import { Container, Grid, Segment } from 'semantic-ui-react'; import AdminPage from '../components/AdminPage'; +import HeapSpace from '../widgets/HeapSpace'; +import SystemStatus from '../widgets/SystemStatus'; const Dashboard: ComponentType = () => ( - Dashboard + + + + + + + + + + + + + + ); diff --git a/client/src/pages/Resource.js b/client/src/pages/Resource.js index b4d4fe9..b0b2e86 100644 --- a/client/src/pages/Resource.js +++ b/client/src/pages/Resource.js @@ -21,13 +21,13 @@ import { Segment } from 'semantic-ui-react'; import AttachmentDetails from '../components/AttachmentDetails'; -import AttachmentStatus from '../components/AttachmentStatus'; import AuthenticationService from '../services/Authentication'; import ProjectsService from '../services/Projects'; import ReadOnlyField from '../components/ReadOnlyField'; import ResourceExifModal from '../components/ResourceExifModal'; import ResourcesService from '../services/Resources'; import SimpleEditPage from '../components/SimpleEditPage'; +import StatusIcon from '../components/StatusIcon'; import styles from './Resource.module.css'; import withEditPage from '../hooks/EditPage'; @@ -198,9 +198,9 @@ const ResourceForm = withTranslation()((props) => { onClick={() => setTab(Tabs.content)} > { props.t('Resource.labels.sourceImage') } - { onClick={() => setTab(Tabs.content_converted)} > { props.t('Resource.labels.convertedImage') } - { AuthenticationService.isAdmin() && ( diff --git a/client/src/services/Dashboard.js b/client/src/services/Dashboard.js new file mode 100644 index 0000000..ab747b8 --- /dev/null +++ b/client/src/services/Dashboard.js @@ -0,0 +1,104 @@ +// @flow + +import { BaseService } from '@performant-software/shared-components'; + +/** + * Class responsible for handling all dashboard API requests. + */ +class Dashboard extends BaseService { + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + create(...args: any): Promise { + return Promise.reject(args); + } + + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + delete(...args: any): Promise { + return Promise.reject(args); + } + + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + fetchAll(...args: any): Promise { + return Promise.reject(args); + } + + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + fetchOne(...args: any): Promise { + return Promise.reject(args); + } + + /** + * Returns the dashboard base URL. + * + * @returns {string} + */ + getBaseUrl(): string { + return '/api/dashboard'; + } + + /** + * Calls the `/api/dashboard/heap` API endpoint. + * + * @returns {*} + */ + heap(): Promise { + return this.getAxios().get(`${this.getBaseUrl()}/heap`, null, this.getConfig()); + } + + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + save(...args: any): Promise { + return Promise.reject(args); + } + + /** + * Calls the `/api/dashboard/status` API endpoint. + * + * @returns {*} + */ + status(): Promise { + return this.getAxios().get(`${this.getBaseUrl()}/status`, null, this.getConfig()); + } + + /** + * Not implemented. + * + * @param args + * + * @returns {Promise<*>} + */ + update(...args: any): Promise { + return Promise.reject(args); + } +} + +const DashboardService: Dashboard = new Dashboard(); +export default DashboardService; diff --git a/client/src/widgets/HeapSpace.js b/client/src/widgets/HeapSpace.js new file mode 100644 index 0000000..4dfbbe2 --- /dev/null +++ b/client/src/widgets/HeapSpace.js @@ -0,0 +1,88 @@ +// @flow + +import React, { + useCallback, + useEffect, + useState, + type ComponentType +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Cell, + Legend, + Pie, + PieChart, + Tooltip +} from 'recharts'; +import DashboardService from '../services/Dashboard'; +import Widget from '../components/Widget'; + +const HeapSpace: ComponentType = () => { + const [data, setData] = useState(); + const [loading, setLoading] = useState(false); + + const { t } = useTranslation(); + + /** + * Calls the `/api/dashboard/heap` API endpoint. + * + * @type {(function(): void)|*} + */ + const onLoad = useCallback(() => { + setLoading(true); + + DashboardService + .heap() + .then(({ data: { max, used } }) => setData([{ + name: t('HeapSpace.labels.free'), + value: max - used + }, { + name: t('HeapSpace.labels.used'), + value: used + }])) + .finally(() => setLoading(false)); + }, []); + + /** + * Renders the custom label based on the passed percent. + * + * @type {function({percent: *}): string} + */ + const renderLabel = useCallback(({ percent }) => `${(percent * 100).toFixed(0)}%`, []); + + /** + * Calls the onLoad function when the component is mounted. + */ + useEffect(() => onLoad(), []); + + return ( + + + + + + + + + + + ); +}; + +export default HeapSpace; diff --git a/client/src/widgets/HeapSpace.module.css b/client/src/widgets/HeapSpace.module.css new file mode 100644 index 0000000..e582a76 --- /dev/null +++ b/client/src/widgets/HeapSpace.module.css @@ -0,0 +1,3 @@ +.heapSpace { + +} \ No newline at end of file diff --git a/client/src/widgets/SystemStatus.js b/client/src/widgets/SystemStatus.js new file mode 100644 index 0000000..e892f04 --- /dev/null +++ b/client/src/widgets/SystemStatus.js @@ -0,0 +1,121 @@ +// @flow + +import React, { + useCallback, + useEffect, + useMemo, + useState, + type ComponentType +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { Grid } from 'semantic-ui-react'; +import AttributeView from '../components/AttributeView'; +import DashboardService from '../services/Dashboard'; +import StatusIcon from '../components/StatusIcon'; +import styles from './SystemStatus.module.css'; +import Widget from '../components/Widget'; + +const StatusColors = { + green: 'GREEN', + red: 'RED', + yellow: 'YELLOW' +}; + +const Statuses = { + [StatusColors.green]: 'positive', + [StatusColors.yellow]: 'warning', + [StatusColors.red]: 'negative' +}; + +const SystemStatus: ComponentType = () => { + const [data, setData] = useState(); + const [loading, setLoading] = useState(false); + + const { t } = useTranslation(); + + /** + * Memo-izes the status constant based on the color. + * + * @type {unknown} + */ + const status = useMemo(() => data && Statuses[data.status_color], [data]); + + /** + * Calls the `/api/dashboard/status` API endpoint and sets the data on the state. + */ + const onLoad = useCallback(() => { + setLoading(true); + + DashboardService + .status() + .then((response) => setData(response.data)) + .finally(() => setLoading(false)); + }, []); + + /** + * Calls the onLoad function when the component is mounted. + */ + useEffect(() => onLoad(), [onLoad]); + + return ( + + { t('SystemStatus.title') } + + + )} + > + { data && ( + + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default SystemStatus; diff --git a/client/src/widgets/SystemStatus.module.css b/client/src/widgets/SystemStatus.module.css new file mode 100644 index 0000000..ac792b3 --- /dev/null +++ b/client/src/widgets/SystemStatus.module.css @@ -0,0 +1,3 @@ +.systemStatus .statusIcon { + margin-left: 0.25em; +} \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index aea0cfa..bd45361 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1052,7 +1052,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.1", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -1931,6 +1931,57 @@ dependencies: "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/eslint-scope@^3.7.3": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" @@ -3627,6 +3678,77 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -3662,6 +3784,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -3859,6 +3986,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -4383,7 +4518,7 @@ eventemitter3@^2.0.3: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" integrity sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg== -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.1: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -4485,6 +4620,11 @@ fast-equals@^3.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-3.0.2.tgz#77f80047b381b6cb747130463ccc144e24c44097" integrity sha512-iY0fAmW7fzxHp22VCRLpOgWbsWsF+DJWi1jhc8w+VGlJUiS+KcGZV2A8t+Q9oTQwhG3L1W8Lu/oe3ZyOPdhZjw== +fast-equals@^5.0.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484" + integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw== + fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -5266,6 +5406,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + invariant@2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -7922,7 +8067,7 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== -react-is@^18.2.0: +react-is@^18.2.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -8034,6 +8179,15 @@ react-scripts@5.0.1: optionalDependencies: fsevents "^2.3.2" +react-smooth@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" + integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + react-syntax-highlighter@^15.5.0: version "15.5.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" @@ -8045,6 +8199,16 @@ react-syntax-highlighter@^15.5.0: prismjs "^1.27.0" refractor "^3.6.0" +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-uuid@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-uuid/-/react-uuid-1.0.2.tgz#c77cea91cf38eafb1aaa7910f92621b2ed91b969" @@ -8102,6 +8266,27 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa" + integrity sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^18.3.1" + react-smooth "^4.0.0" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -9079,6 +9264,11 @@ tiny-invariant@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tinycolor2@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" @@ -9346,6 +9536,26 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +victory-vendor@^36.6.8: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + void-elements@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" diff --git a/config/locales/en.yml b/config/locales/en.yml index e32994d..c9fe0e7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -30,8 +30,14 @@ # available at https://guides.rubyonrails.org/i18n.html. en: + dashboard_serializer: + application_name: Cantaloupe + errors: + dashboard_controller: + unauthorized: "You do not have access to the system dashboard." + presentation_controller: collection_invalid: "Invalid parameters for creating a IIIF collection." manifest_invalid: "Invalid parameters for creating a IIIF manifest." diff --git a/config/routes.rb b/config/routes.rb index 5f63c4a..bb6cb58 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,9 @@ mount UserDefinedFields::Engine, at: '/user_defined_fields' namespace :api do + get 'dashboard/heap', to: 'dashboard#heap' + get 'dashboard/status', to: 'dashboard#status' + resources :organizations resources :projects resources :resources do