diff --git a/apps/sensenet/package.json b/apps/sensenet/package.json index 364a22ab6..00dcd6ecf 100644 --- a/apps/sensenet/package.json +++ b/apps/sensenet/package.json @@ -99,10 +99,12 @@ "clsx": "^1.1.1", "date-fns": "^2.22.1", "filesize": "^6.3.0", + "frappe-charts": "^1.6.1", "react": "^16.13.0", "react-autosuggest": "^10.1.0", "react-day-picker": "^7.4.10", "react-dom": "^16.13.0", + "react-frappe-charts": "^4.0.0", "react-markdown": "^6.0.2", "react-monaco-editor": "0.43.0", "react-responsive": "^8.2.0", diff --git a/apps/sensenet/src/components/settings/stats-usage-widget.tsx b/apps/sensenet/src/components/settings/stats-usage-widget.tsx new file mode 100644 index 000000000..71d4b365e --- /dev/null +++ b/apps/sensenet/src/components/settings/stats-usage-widget.tsx @@ -0,0 +1,134 @@ +import { formatSize } from '@sensenet/controls-react' +import { useRepository } from '@sensenet/hooks-react' +import { createStyles, ListItemText, makeStyles, MenuItem, Paper, Select } from '@material-ui/core' +import React, { useEffect, useState } from 'react' +import ReactFrappeChart from 'react-frappe-charts' +import { widgetStyles } from '../../globalStyles' +import { useLocalization } from '../../hooks' +import { useDateUtils } from '../../hooks/use-date-utils' +import { FullScreenLoader } from '../full-screen-loader' + +export type PeriodData = { + PeriodStartDate: Date + PeriodEndDate: Date +} + +export type ApiPeriod = { + DataType: String + Start: String + End: String + TimeWindow: 'Hour' | 'Day' | 'Month' | 'Year' + Resolution: 'Minute' | 'Hour' | 'Day' | 'Month' + CallCount: number[] + RequestLengths: number[] + ResponseLengths: number[] +} + +const useWidgetStyles = makeStyles(widgetStyles) + +const useStyles = makeStyles(() => { + return createStyles({ + rowContainer: { + padding: '16px 0', + }, + usageContainer: { + display: 'flex', + flexFlow: 'row', + }, + leftContent: { + width: '60%', + backgroundColor: '#252525', + }, + rightContent: { + width: '40%', + paddingLeft: '40px', + }, + }) +}) + +export interface UsageWidgetProps { + periodData: PeriodData[] +} + +export const UsageWidget: React.FunctionComponent = (props) => { + const classes = useStyles() + const widgetClasses = useWidgetStyles() + const localization = useLocalization().settings + const dateUtils = useDateUtils() + const repository = useRepository() + const [currentPeriod, setCurrentPeriod] = useState(props.periodData[props.periodData.length - 1]) + const [currentData, setCurrentData] = useState() + + const dataTraffic = currentData?.RequestLengths.map((request, index) => request + currentData.ResponseLengths[index]) + + useEffect(() => { + ;(async () => { + const response = await repository.executeAction({ + idOrPath: '/Root', + name: 'GetApiUsagePeriod', + method: 'POST', + body: { + timeWindow: 'Month', + }, + }) + + setCurrentData(response) + })() + }, [repository]) + + if (!dataTraffic) return + if (!currentPeriod) return null + + return ( +
+ +
+ {localization.traffic} +
+
+
+ (index % 5 ? '' : index.toString())), + datasets: [{ values: dataTraffic }], + }} + /> +
+
+ +
{localization.dataTraffic}
+
{formatSize(dataTraffic.reduce((a, b) => a + b, 0))}
+
{localization.apiCalls}
+
{currentData?.CallCount.reduce((a, b) => a + b, 0)}
+
+
+
+
+ ) +} diff --git a/apps/sensenet/src/components/settings/stats.tsx b/apps/sensenet/src/components/settings/stats.tsx index 54ac553d6..b269ee32f 100644 --- a/apps/sensenet/src/components/settings/stats.tsx +++ b/apps/sensenet/src/components/settings/stats.tsx @@ -1,6 +1,7 @@ import { useRepository, VersionInfo } from '@sensenet/hooks-react' import { Container } from '@material-ui/core' import clsx from 'clsx' +import { addMonths, isSameDay, parseISO } from 'date-fns' import React, { useEffect, useState } from 'react' import { useGlobalStyles } from '../../globalStyles' import { useLocalization } from '../../hooks' @@ -9,6 +10,40 @@ import { FullScreenLoader } from '../full-screen-loader' import { ComponentsWidget } from './stats-components-widget' import { InstalledPackagesWidget } from './stats-installed-packages-widget' import { StorageWidget } from './stats-storage-widget' +import { UsageWidget } from './stats-usage-widget' + +const makePeriodArrayFromRawData = (periodData: RawPeriodData) => { + const periodArray = [] + const rawData = periodData + + let currentDate = parseISO(rawData.First) + const lastDate = parseISO(rawData.Last) + + while (!isSameDay(currentDate, lastDate)) { + const endDate = addMonths(currentDate, 1) + periodArray.push({ + PeriodStartDate: currentDate, + PeriodEndDate: endDate, + }) + currentDate = endDate + } + if (periodArray.length < rawData.Count) { + periodArray.push({ + PeriodStartDate: lastDate, + PeriodEndDate: new Date(Date.now()), + }) + } + + return periodArray +} + +export type RawPeriodData = { + Window: 'Hour' | 'Day' | 'Month' | 'Year' + Resolution: 'Minute' | 'Hour' | 'Day' | 'Month' + First: string + Last: string + Count: number +} export const Stats: React.FunctionComponent = () => { const globalClasses = useGlobalStyles() @@ -16,6 +51,22 @@ export const Stats: React.FunctionComponent = () => { const repository = useRepository() const [versionInfo, setVersionInfo] = useState() const [dashboardData, setDashboardData] = useState() + const [periodData, setPeriodData] = useState() + + useEffect(() => { + ;(async () => { + const response = await repository.executeAction({ + idOrPath: '/Root', + name: 'GetApiUsagePeriods', + method: 'POST', + body: { + timeWindow: 'Month', + }, + }) + + setPeriodData(response) + })() + }, [repository]) useEffect(() => { ;(async () => { @@ -44,7 +95,7 @@ export const Stats: React.FunctionComponent = () => { })() }, [repository]) - if (!versionInfo || !dashboardData) return + if (!versionInfo || !dashboardData || !periodData) return return (
@@ -53,6 +104,7 @@ export const Stats: React.FunctionComponent = () => {
+ diff --git a/apps/sensenet/src/hooks/use-date-utils.ts b/apps/sensenet/src/hooks/use-date-utils.ts index d3321c872..d530805a9 100644 --- a/apps/sensenet/src/hooks/use-date-utils.ts +++ b/apps/sensenet/src/hooks/use-date-utils.ts @@ -1,6 +1,4 @@ -import format from 'date-fns/format' -import formatDistanceToNow from 'date-fns/formatDistanceToNow' -import parseISO from 'date-fns/parseISO' +import { format, formatDistanceToNow, parseISO } from 'date-fns' import { useCallback } from 'react' import { LocalizationObject } from '../context' import { usePersonalSettings } from '.' diff --git a/apps/sensenet/src/localization/default.ts b/apps/sensenet/src/localization/default.ts index 3e440feb9..e950b4fe0 100644 --- a/apps/sensenet/src/localization/default.ts +++ b/apps/sensenet/src/localization/default.ts @@ -469,6 +469,9 @@ const values = { installedPackagesInfo: 'These packages are mainly the building bricks of sensenet components. There are tool-like packages that are not part of the component structure, they were made to run multiple times, for example delete or index content.', notAvailable: 'Not available', + traffic: 'Traffic', + dataTraffic: 'Data traffic (request and response)', + apiCalls: 'Number of requests (API calls)', }, customActions: { executeCustomActionDialog: { diff --git a/apps/sensenet/src/localization/hungarian.ts b/apps/sensenet/src/localization/hungarian.ts index cd0082f34..9daefce3b 100644 --- a/apps/sensenet/src/localization/hungarian.ts +++ b/apps/sensenet/src/localization/hungarian.ts @@ -200,6 +200,9 @@ const values: Localization = { installedPackagesInfo: 'Csomagok, melyekből a sensenet komponensek felépülnek. Léteznek olyan csomagok is, melyek nem felelősek a komponensekért, többszöri futtatásra lettek létrehozva - ilyenek a tool-típusú csomagok. Ezek például kontent törlésre vagy indexelésre használatosak.', notAvailable: 'Nem elérhető', + traffic: 'Adatforgalom', + dataTraffic: 'Adatforgalom (kérések és válaszok)', + apiCalls: 'Kérések száma (API hívások)', }, forms: { referencePicker: 'Referencia választó', diff --git a/yarn.lock b/yarn.lock index ead988d7f..66c1a9694 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11907,6 +11907,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +frappe-charts@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.6.1.tgz#2162cec05f4524b10cf232df8787a3ac20218384" + integrity sha512-Fteae/oqv4XdxP4ALoqTmUBBvXt8ECtghziqabp1ZA4yLqY8a3haafNqluwUCxnBOvrV+EdpvhZu0H+VEebX1Q== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -20749,6 +20754,11 @@ react-focus-lock@^2.1.0: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" +react-frappe-charts@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/react-frappe-charts/-/react-frappe-charts-4.0.0.tgz#5a9958641ac0e74ce4c44a23788a5e879ac716a7" + integrity sha512-QmYY1ExlPidE8DK3jtjGWakP0YptHOlFMoYF4/Qxsbrw5hYMCwocPoco4b3e2qk6mI0trdpzILzpjmFWutO/MA== + react-helmet-async@^1.0.2, react-helmet-async@^1.0.7: version "1.0.9" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.9.tgz#5b9ed2059de6b4aab47f769532f9fbcbce16c5ca"