diff --git a/web_ui/frontend/app/cache/metrics/page.tsx b/web_ui/frontend/app/cache/metrics/page.tsx
index 37d28fd3a..985ad52e7 100644
--- a/web_ui/frontend/app/cache/metrics/page.tsx
+++ b/web_ui/frontend/app/cache/metrics/page.tsx
@@ -1,153 +1,3 @@
-import { Box, Grid, Paper, Typography } from '@mui/material';
-import { green, grey, blue } from '@mui/material/colors';
-
-import {
- BigMetric,
- ProjectTable,
- BigBytesMetric,
- StorageGraph,
- TransferRateGraph,
- CPUGraph,
- MemoryGraph,
-} from '@/components/metrics';
-
-const Page = () => {
- return (
-
-
-
- {[
- ,
- ,
- ,
- ].map((component, index) => (
-
- {component}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
+import CacheMetricPage from '@/components/graphs/CacheMetricPage';
+const Page = () => ;
export default Page;
diff --git a/web_ui/frontend/app/cache/page.tsx b/web_ui/frontend/app/cache/page.tsx
index 6f9b4618d..6192abfd1 100644
--- a/web_ui/frontend/app/cache/page.tsx
+++ b/web_ui/frontend/app/cache/page.tsx
@@ -20,11 +20,9 @@
import { Box, Grid, Typography } from '@mui/material';
-import RateGraph from '@/components/graphs/RateGraph';
import StatusBox from '@/components/StatusBox';
import { DataPoint, TimeDuration } from '@/components/graphs/prometheus';
import FederationOverview from '@/components/FederationOverview';
-import LineGraph from '@/components/graphs/LineGraph';
export default function Home() {
return (
diff --git a/web_ui/frontend/app/director/components/DirectorCard.tsx b/web_ui/frontend/app/director/components/DirectorCard.tsx
index f99652142..3a42b9b1d 100644
--- a/web_ui/frontend/app/director/components/DirectorCard.tsx
+++ b/web_ui/frontend/app/director/components/DirectorCard.tsx
@@ -16,7 +16,7 @@ import {
} from '@mui/material';
import { red, grey } from '@mui/material/colors';
import { Server } from '@/index';
-import { Language } from '@mui/icons-material';
+import { Equalizer, Language } from '@mui/icons-material';
import { NamespaceIcon } from '@/components/Namespace/index';
import useSWR from 'swr';
import Link from 'next/link';
@@ -80,7 +80,10 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
- {server.name}
+
+ {server.name}
+ {server?.version && <> • {server.version}>}
+
@@ -136,6 +139,22 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
)}
+ {authenticated &&
+ authenticated.role == 'admin' &&
+ server?.webUrl && (
+
+
+
+
+
+
+
+
+
+ )}
diff --git a/web_ui/frontend/app/director/metrics/cache/page.tsx b/web_ui/frontend/app/director/metrics/cache/page.tsx
new file mode 100644
index 000000000..25fbfa9c9
--- /dev/null
+++ b/web_ui/frontend/app/director/metrics/cache/page.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import CacheMetricPage from '@/components/graphs/CacheMetricPage';
+import { Suspense } from 'react';
+import { Skeleton } from '@mui/material';
+import { useSearchParams } from 'next/navigation';
+
+const RemoteCachePage = () => {
+ const params = useSearchParams();
+ const serverName = params.get('server_name') || undefined;
+
+ return ;
+};
+
+const Page = () => {
+ return (
+ }>
+
+
+ );
+};
+
+export default Page;
diff --git a/web_ui/frontend/app/director/metrics/components/MetricBoxPlot.tsx b/web_ui/frontend/app/director/metrics/components/MetricBoxPlot.tsx
index ee2ad7217..66a31bea5 100644
--- a/web_ui/frontend/app/director/metrics/components/MetricBoxPlot.tsx
+++ b/web_ui/frontend/app/director/metrics/components/MetricBoxPlot.tsx
@@ -1,5 +1,6 @@
'use client';
+import Link from 'next/link';
import { Chart } from 'react-chartjs-2';
import {
CategoryScale,
@@ -19,6 +20,7 @@ import {
TooltipLabelStyle,
TooltipItem,
} from 'chart.js';
+import { getRelativePosition } from 'chart.js/helpers';
import { useContext, useEffect, useMemo, useState } from 'react';
import {
BoxAndWiskers,
@@ -42,7 +44,15 @@ import {
toBytes,
toBytesString,
} from '@/helpers/bytes';
-import { evaluateOrReturn, TypeOrTypeFunction } from '@/helpers/util';
+import {
+ alertOnError,
+ evaluateOrReturn,
+ TypeOrTypeFunction,
+} from '@/helpers/util';
+import { useRouter } from 'next/navigation';
+import { ServerGeneral, ServerType } from '@/types';
+import { getDirectorServers } from '@/helpers/get';
+import { AlertDispatchContext } from '@/components/AlertProvider';
ChartJS.register(
BoxPlotController,
@@ -69,6 +79,7 @@ export const BytesMetricBoxPlot = ({
title: string;
options?: ChartOptions;
}) => {
+ const router = useRouter();
const { rate, time, resolution, range } = useContext(GraphContext);
const { data } = useSWR(
@@ -118,6 +129,22 @@ export const BytesMetricBoxPlot = ({
type: 'logarithmic',
},
},
+ onClick: function (evt: any, item: any) {
+ if (item.length === 0) {
+ return;
+ }
+ const position = getRelativePosition(evt, evt.Chart) as any; // Typing is wrong
+ try {
+ const serverName =
+ position.chart.tooltip.body[0].lines[0].split(':')[0];
+ if (serverName != 'Missing Server Name') {
+ router.push(
+ '/director/metrics/origin/?server_name=' +
+ position.chart.tooltip.body[0].lines[0].split(':')[0]
+ );
+ }
+ } catch {}
+ },
plugins: {
tooltip: {
callbacks: {
@@ -153,7 +180,9 @@ export const MetricBoxPlot = ({
title: string;
options?: ChartOptions;
}) => {
+ const router = useRouter();
const { rate, time, resolution, range } = useContext(GraphContext);
+ const dispatch = useContext(AlertDispatchContext);
const { data } = useSWR(
[metric, rate, time, resolution, range],
@@ -163,6 +192,16 @@ export const MetricBoxPlot = ({
}
);
+ const { data: servers } = useSWR(
+ 'getDirectorServers',
+ async () =>
+ await alertOnError(
+ getDirectorServers,
+ 'Failed to fetch servers',
+ dispatch
+ )
+ );
+
const chartData = useMemo(() => {
return {
labels: [`${title}`],
@@ -187,6 +226,30 @@ export const MetricBoxPlot = ({
type: 'logarithmic',
},
},
+ onClick: function (evt: any, item: any) {
+ if (item.length === 0) {
+ return;
+ }
+ const position = getRelativePosition(evt, evt.Chart) as any; // Typing is broken here for the Chartjs library
+ try {
+ const serverName =
+ position.chart.tooltip.body[0].lines[0].split(':')[0];
+ if (serverName != 'Missing Server Name') {
+ let serverType: ServerType = 'Cache';
+ if (servers) {
+ const server = servers.find((s) => s.name == serverName);
+ if (server) {
+ serverType = server.type;
+ }
+ }
+
+ router.push(
+ `/director/metrics/${serverType.toLowerCase()}/?server_name=` +
+ position.chart.tooltip.body[0].lines[0].split(':')[0]
+ );
+ }
+ } catch {}
+ },
plugins: {
tooltip: {
callbacks: {
@@ -241,6 +304,7 @@ export const getMetricData = async (
return result?.metric?.server_name || 'Missing Server Name';
});
+ // console.log(data);
return {
data,
labels,
diff --git a/web_ui/frontend/app/director/metrics/components/ServerUptime.tsx b/web_ui/frontend/app/director/metrics/components/ServerUptime.tsx
index f25d30163..262c8324d 100644
--- a/web_ui/frontend/app/director/metrics/components/ServerUptime.tsx
+++ b/web_ui/frontend/app/director/metrics/components/ServerUptime.tsx
@@ -31,6 +31,9 @@ import {
} from '@mui/material';
import { AlertDispatchContext } from '@/components/AlertProvider';
import { alertOnError } from '@/helpers/util';
+import Link from 'next/link';
+import { ServerType } from '@/types';
+import StyledTableCell from '@/components/StyledHeadTableCell';
const ServerUptime = () => {
const dispatch = useContext(AlertDispatchContext);
@@ -53,24 +56,28 @@ const ServerUptime = () => {
data = useMemo(() => (data ? data : []), [data]);
return (
-
+ <>
{data.length === 0 && !isLoading && !isValidating && (
No data available
)}
-
-
+
+
- Server
- Status
- Restarts
+ Server
+ Status
+ Restarts
{data.map((d) => (
- {d.serverName}
+
+ {d.serverName}
+
{
-
+ >
);
};
interface ServerUptimeData {
serverName: string;
+ serverType: ServerType;
ranges: Range[];
points: Point[];
}
@@ -133,16 +141,18 @@ export const getMetricData = async (
let uptimes: ServerUptimeData[] = countResponseFilled.result.map((result) => {
const serverName = result.metric.server_name;
+ const serverType = (result.metric.server_type || 'Cache') as ServerType; // Default to Cache which subsets the Origin metrics
const ranges = countResponseToRanges(result);
const restartServer = restartResponse.data.result.filter(
(r) => r.metric.server_name === serverName
);
if (restartServer.length === 0) {
- return { serverName, ranges, points: [] };
+ return { serverName, serverType, ranges, points: [] };
}
return {
serverName,
+ serverType,
ranges,
points: restartResponseToPoints(restartServer[0]),
};
diff --git a/web_ui/frontend/app/director/metrics/components/StorageTable.tsx b/web_ui/frontend/app/director/metrics/components/StorageTable.tsx
index 0b1413a45..9f9dc6085 100644
--- a/web_ui/frontend/app/director/metrics/components/StorageTable.tsx
+++ b/web_ui/frontend/app/director/metrics/components/StorageTable.tsx
@@ -1,5 +1,6 @@
'use client';
+import Link from 'next/link';
import {
Box,
Table,
@@ -8,6 +9,7 @@ import {
TableContainer,
TableHead,
TableRow,
+ styled,
} from '@mui/material';
import {
MatrixResponseData,
@@ -17,11 +19,14 @@ import {
} from '@/components/graphs/prometheus';
import useSWR from 'swr';
-import { useContext, useMemo } from 'react';
+import { ReactNode, useContext, useMemo } from 'react';
import { GraphContext } from '@/components/graphs/GraphContext';
import { DateTime } from 'luxon';
import chroma from 'chroma-js';
import { convertToBiggestBytes, toBytes, toBytesString } from '@/helpers/bytes';
+import { ServerType } from '@/types';
+import { tableCellClasses } from '@mui/material/TableCell';
+import StyledTableCell from '@/components/StyledHeadTableCell';
export const StorageTable = () => {
const { rate, time, range, resolution } = useContext(GraphContext);
@@ -67,87 +72,101 @@ export const StorageTable = () => {
return (
<>
{storageData !== undefined && (
-
-
-
-
-
- Server
- Used ({totalUsedByteString})
- Free ({totalFreeByteString})
-
-
-
- {Object.values(storageData)
- .sort((a, b) => {
- const nameA = a.serverName.toUpperCase();
- const nameB = b.serverName.toUpperCase();
- return nameA > nameB ? 1 : nameA < nameB ? -1 : 0;
- })
- .map((d) => (
-
- {d.serverName}
-
+
+
+
+ Server
+ Used ({totalUsedByteString})
+ Free ({totalFreeByteString})
+
+
+
+ {Object.values(storageData)
+ .sort((a, b) => {
+ const nameA = a.serverName.toUpperCase();
+ const nameB = b.serverName.toUpperCase();
+ return nameA > nameB ? 1 : nameA < nameB ? -1 : 0;
+ })
+ .map((d) => (
+
+
+
- {toBytesString(d.total - d.free)}
-
-
- {toBytesString(d.free)}
-
-
- ))}
-
-
-
-
+ {d.serverName}
+
+
+
+ {toBytesString(d.total - d.free)}
+
+
+ {toBytesString(d.free)}
+
+
+ ))}
+
+
+
)}
>
);
};
+interface StorageMetric {
+ serverName: string;
+ serverType: ServerType;
+ free: number;
+ total: number;
+}
+
const getStorageData = async (
time: DateTime
-): Promise<
- Record
-> => {
+): Promise> => {
const query = `xrootd_storage_volume_bytes`;
const response = await query_raw(query, time.toSeconds());
const result = response.data.result;
- return result.reduce((acc: Record, r) => {
- const serverName = r.metric.server_name;
- const type = r.metric.type;
+ return result.reduce(
+ (acc: Record, r): Record => {
+ const serverName = r.metric.server_name;
+ const serverType = (r.metric?.server_type || 'Cache') as ServerType; // Default to Cache which subsets the Origin metrics
+ const type = r.metric.type;
- if (serverName === undefined) {
- return acc;
- }
+ if (serverName === undefined) {
+ return acc;
+ }
- acc[serverName] = {
- ...acc?.[serverName],
- serverName: serverName,
- [type]: Number(r.value[1]),
- };
+ acc[serverName] = {
+ ...acc?.[serverName],
+ [type]: Number(r.value[1]),
+ serverName,
+ serverType,
+ };
- return acc;
- }, {});
+ return acc;
+ },
+ {}
+ );
};
diff --git a/web_ui/frontend/app/director/metrics/components/TransferBarGraph.tsx b/web_ui/frontend/app/director/metrics/components/TransferBarGraph.tsx
index d6c842470..7c33d88a7 100644
--- a/web_ui/frontend/app/director/metrics/components/TransferBarGraph.tsx
+++ b/web_ui/frontend/app/director/metrics/components/TransferBarGraph.tsx
@@ -55,7 +55,7 @@ const TransferBarGraph = () => {
const { data } = useSWR>(
[
- 'transferRateGraph',
+ 'transferBarGraph',
graphContext.rate,
graphContext.range,
graphContext.resolution,
diff --git a/web_ui/frontend/app/director/metrics/origin/page.tsx b/web_ui/frontend/app/director/metrics/origin/page.tsx
new file mode 100644
index 000000000..f4bf0099d
--- /dev/null
+++ b/web_ui/frontend/app/director/metrics/origin/page.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import OriginMetricPage from '@/components/graphs/OriginMetricPage';
+import { Suspense, useState, useEffect } from 'react';
+import { Skeleton } from '@mui/material';
+import { useSearchParams } from 'next/navigation';
+
+const RemoteOriginPage = () => {
+ const params = useSearchParams();
+ const serverName = params.get('server_name') || undefined;
+
+ return ;
+};
+
+const Page = () => {
+ return (
+ }>
+
+
+ );
+};
+
+export default Page;
diff --git a/web_ui/frontend/app/director/metrics/page.tsx b/web_ui/frontend/app/director/metrics/page.tsx
index 806f181dd..a8cc108ec 100644
--- a/web_ui/frontend/app/director/metrics/page.tsx
+++ b/web_ui/frontend/app/director/metrics/page.tsx
@@ -25,6 +25,7 @@ const Page = () => {
{[
,
,
+ ,
].map((component, index) => (
{component}
@@ -37,6 +38,16 @@ const Page = () => {
{[
+ ,
+ ,
{
- return (
-
-
-
- {[
- ,
- ,
- ,
- ,
- ].map((component, index) => (
-
- {component}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
+import OriginMetricPage from '@/components/graphs/OriginMetricPage';
+const Page = () => ;
export default Page;
diff --git a/web_ui/frontend/app/origin/page.tsx b/web_ui/frontend/app/origin/page.tsx
index 739b991a3..2911647e9 100644
--- a/web_ui/frontend/app/origin/page.tsx
+++ b/web_ui/frontend/app/origin/page.tsx
@@ -29,7 +29,6 @@ import {
} from '@mui/material';
import { Key, CheckCircle } from '@mui/icons-material';
-import RateGraph from '@/components/graphs/RateGraph';
import StatusBox from '@/components/StatusBox';
import { DataExportTable } from '@/components/DataExportTable';
import { TimeDuration } from '@/components/graphs/prometheus';
diff --git a/web_ui/frontend/components/CardList.tsx b/web_ui/frontend/components/CardList.tsx
index 31a10339e..d6031394c 100644
--- a/web_ui/frontend/components/CardList.tsx
+++ b/web_ui/frontend/components/CardList.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import React, {
ComponentType,
FunctionComponent,
diff --git a/web_ui/frontend/components/DataTable.tsx b/web_ui/frontend/components/DataTable.tsx
deleted file mode 100644
index 2388add8e..000000000
--- a/web_ui/frontend/components/DataTable.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { ReactElement, useMemo } from 'react';
-import {
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
-} from '@mui/material';
-
-export interface ColumnMap {
- [key: string]: Column;
-}
-
-export interface Column {
- name: string;
- cellNode: React.JSX.ElementType;
-}
-
-export interface Record {
- [key: string]: string | number | boolean | null;
-}
-
-const DataTable = ({
- columnMap,
- data,
-}: {
- columnMap: ColumnMap;
- data: Record[];
-}): ReactElement => {
- // If there is data then show, if not then indicate no data
- const rows = useMemo(() => {
- if (data.length !== 0) {
- return data.map((record, index) => (
-
- {Object.entries(columnMap).map(([key, column], index) => {
- const CellNode = column.cellNode;
- return {record[key]};
- })}
-
- ));
- } else {
- return (
-
- {Object.entries(columnMap).map(([key, column], index) => {
- return No Data;
- })}
-
- );
- }
- }, [data]);
-
- return (
-
-
-
-
- {Object.values(columnMap).map((column, index) => (
- {column.name}
- ))}
-
-
- {rows}
-
-
- );
-};
-
-export default DataTable;
diff --git a/web_ui/frontend/components/Namespace/Card.tsx b/web_ui/frontend/components/Namespace/Card.tsx
index d8a13935d..131b1faff 100644
--- a/web_ui/frontend/components/Namespace/Card.tsx
+++ b/web_ui/frontend/components/Namespace/Card.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import { Alert, Alert as AlertType, RegistryNamespace } from '@/index';
import React, { useContext, useRef, useState } from 'react';
import {
diff --git a/web_ui/frontend/components/Namespace/NamespaceCardList.tsx b/web_ui/frontend/components/Namespace/NamespaceCardList.tsx
index b8770b529..f2cea5748 100644
--- a/web_ui/frontend/components/Namespace/NamespaceCardList.tsx
+++ b/web_ui/frontend/components/Namespace/NamespaceCardList.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import React, {
ComponentType,
FunctionComponent,
diff --git a/web_ui/frontend/components/Namespace/PendingCard.tsx b/web_ui/frontend/components/Namespace/PendingCard.tsx
index e99451d5c..f9fbd26be 100644
--- a/web_ui/frontend/components/Namespace/PendingCard.tsx
+++ b/web_ui/frontend/components/Namespace/PendingCard.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import React, { useContext, useMemo, useRef, useState } from 'react';
import { Authenticated, secureFetch } from '@/helpers/login';
import { Avatar, Box, IconButton, Tooltip, Typography } from '@mui/material';
diff --git a/web_ui/frontend/components/ServerTable.tsx b/web_ui/frontend/components/ServerTable.tsx
deleted file mode 100644
index bcdb65b54..000000000
--- a/web_ui/frontend/components/ServerTable.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import {
- Table,
- TableCell,
- TableBody,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Typography,
- Box,
-} from '@mui/material';
-import React, {
- FunctionComponent,
- ReactElement,
- ReactNode,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react';
-import { Skeleton } from '@mui/material';
-import Link from 'next/link';
-
-import DataTable, { Record } from '@/components/DataTable';
-import { TableCellOverflow } from '@/components/Cell';
-import { getErrorMessage } from '@/helpers/util';
-
-interface ExportData extends Record {
- Type: string;
- 'Local Path': string;
- 'Namespace Prefix': string;
-}
-
-const TableCellOverflowLink: React.JSX.ElementType = ({
- children,
- ...props
-}) => {
- if (children === null) {
- children = '';
- }
-
- return (
-
- {children}
-
- );
-};
-
-interface Server extends Record {
- name: string;
- authUrl: string;
- url: string;
- webUrl: string;
- type: string;
- latitude: number;
- longitude: number;
-}
-
-interface ServerTableProps {
- type?: 'cache' | 'origin';
-}
-
-export const ServerTable = ({ type }: ServerTableProps) => {
- const [data, setData] = useState(undefined);
- const [error, setError] = useState(undefined);
-
- const keyToName = {
- name: {
- name: 'Name',
- cellNode: TableCellOverflow,
- },
- authUrl: {
- name: 'Auth URL',
- cellNode: TableCellOverflowLink,
- },
- url: {
- name: 'URL',
- cellNode: TableCellOverflowLink,
- },
- webUrl: {
- name: 'Web URL',
- cellNode: TableCellOverflowLink,
- },
- };
-
- const getData = useCallback(async () => {
- const url = new URL(
- '/api/v1.0/director_ui/servers',
- window.location.origin
- );
- if (type) {
- url.searchParams.append('server_type', type);
- }
-
- let response = await fetch(url);
- if (response.ok) {
- const responseData: Server[] = await response.json();
- responseData.sort((a, b) => a.name.localeCompare(b.name));
- setData(responseData);
- } else {
- setError(await getErrorMessage(response));
- }
- }, [type]);
-
- useEffect(() => {
- getData();
- }, []);
-
- if (error) {
- return (
-
-
- {error}
-
-
- );
- }
-
- return (
-
- {data ? (
-
- ) : (
-
- )}
-
- );
-};
diff --git a/web_ui/frontend/components/StyledHeadTableCell.tsx b/web_ui/frontend/components/StyledHeadTableCell.tsx
new file mode 100644
index 000000000..2bc88bef3
--- /dev/null
+++ b/web_ui/frontend/components/StyledHeadTableCell.tsx
@@ -0,0 +1,20 @@
+import { styled, TableCell } from '@mui/material';
+import { tableCellClasses } from '@mui/material/TableCell';
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+ [`&.${tableCellClasses.head}`]: {
+ backgroundColor: theme.palette.primary.main,
+ color: theme.palette.primary.contrastText,
+ },
+ [`&.${tableCellClasses.head}:last-of-type`]: {
+ borderRadius: '0 5px 0 0',
+ },
+ [`&.${tableCellClasses.head}:first-of-type`]: {
+ borderRadius: '5px 0 0 0',
+ },
+ [`&.${tableCellClasses.body}`]: {
+ fontSize: 14,
+ },
+}));
+
+export default StyledTableCell;
diff --git a/web_ui/frontend/components/graphs/CacheMetricPage.tsx b/web_ui/frontend/components/graphs/CacheMetricPage.tsx
new file mode 100644
index 000000000..219523297
--- /dev/null
+++ b/web_ui/frontend/components/graphs/CacheMetricPage.tsx
@@ -0,0 +1,179 @@
+import { Box, Grid, Paper, Typography } from '@mui/material';
+import { green, grey, blue } from '@mui/material/colors';
+
+import {
+ BigMetric,
+ ProjectTable,
+ BigBytesMetric,
+ TransferRateGraph,
+ CPUGraph,
+ MemoryGraph,
+ StorageGraph,
+} from '@/components/metrics';
+import { buildMetric as bm } from '@/components';
+
+export const CacheMetricPage = ({
+ server_name = undefined,
+}: {
+ server_name?: string;
+}) => {
+ return (
+
+
+
+ {[
+ ,
+ ,
+ ,
+ ].map((component, index) => (
+
+ {component}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CacheMetricPage;
diff --git a/web_ui/frontend/components/graphs/Graph.tsx b/web_ui/frontend/components/graphs/Graph.tsx
deleted file mode 100644
index a263dea05..000000000
--- a/web_ui/frontend/components/graphs/Graph.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-/***************************************************************
- *
- * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you
- * may not use this file except in compliance with the License. You may
- * obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- ***************************************************************/
-
-'use client';
-
-import { useEffect, useRef, useState } from 'react';
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- TimeScale,
- PointElement,
- LineElement,
- Title,
- Tooltip,
- Legend,
- ChartOptions,
- Colors,
-} from 'chart.js';
-
-import zoomPlugin from 'chartjs-plugin-zoom';
-import 'chartjs-adapter-luxon';
-
-import { BoxProps } from '@mui/material';
-
-import { Line } from 'react-chartjs-2';
-import { Box, Skeleton, Typography } from '@mui/material';
-
-import { getDataWrapperFunction } from '@/components/graphs/prometheus';
-import { ChartData } from 'chart.js';
-import useSWR from 'swr';
-
-const defaultOptions: Partial> = {
- scales: {
- x: {
- type: 'time',
- time: {
- round: 'second',
- },
- },
- },
-};
-
-interface GraphProps {
- getData: getDataWrapperFunction;
- drawer?: any;
- options?: ChartOptions<'line'>;
- boxProps?: BoxProps;
-}
-
-export default function Graph({
- getData,
- options,
- boxProps,
- drawer,
-}: GraphProps) {
- const randomString = useRef(Math.random().toString(36).substring(7));
- const { data, isLoading, error, mutate } = useSWR(
- 'projectData' + randomString.current,
- getData
- );
-
- // Anytime the getter changes lets update the data accordingly
- useEffect(() => {
- mutate();
- }, [getData]);
-
- useEffect(() => {
- ChartJS.register(
- CategoryScale,
- LinearScale,
- PointElement,
- LineElement,
- Title,
- Tooltip,
- Legend,
- TimeScale,
- zoomPlugin,
- Colors
- );
- }, []);
-
- return (
-
- {isLoading || !data ? (
-
-
-
- ) : (
- <>
-
-
-
- {drawer ? drawer : undefined}
- >
- )}
- {error && (
-
-
- {JSON.stringify(error)}
-
-
- )}
-
- );
-}
diff --git a/web_ui/frontend/components/graphs/GraphContext.tsx b/web_ui/frontend/components/graphs/GraphContext.tsx
index d222cc515..bcb065a63 100644
--- a/web_ui/frontend/components/graphs/GraphContext.tsx
+++ b/web_ui/frontend/components/graphs/GraphContext.tsx
@@ -154,10 +154,19 @@ function graphReducer(state: GraphContextType, action: GraphReducerAction) {
// Update the current windows url to reflect the new state
if (window !== undefined) {
+ // Read in the current url params
+ const urlParams = new URLSearchParams(window.location.search);
+ // Update the time and range params
+ let time = newState.time.toISO();
+ if (time) {
+ urlParams.set('time', time);
+ }
+ urlParams.set('range', newState.range.toString());
+
window.history.pushState(
{},
'',
- `?time=${newState.time.toISO()}&range=${newState.range.toString()}`
+ urlParams.toString() ? `?${urlParams.toString()}` : ''
);
}
diff --git a/web_ui/frontend/components/graphs/LineGraph.tsx b/web_ui/frontend/components/graphs/LineGraph.tsx
deleted file mode 100644
index e0a596ab9..000000000
--- a/web_ui/frontend/components/graphs/LineGraph.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-/***************************************************************
- *
- * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you
- * may not use this file except in compliance with the License. You may
- * obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- ***************************************************************/
-
-'use client';
-
-import dynamic from 'next/dynamic';
-import React, { useCallback, useState } from 'react';
-import { ChartOptions, ChartDataset } from 'chart.js';
-import { ChartData } from 'chart.js';
-import { DateTime } from 'luxon';
-import { BoxProps, Grid } from '@mui/material';
-
-import {
- query_basic,
- query_rate,
- TimeDuration,
-} from '@/components/graphs/prometheus';
-import {
- GraphDrawer,
- RateInput,
- ResolutionInput,
-} from '@/components/graphs/Drawer';
-const Graph = dynamic(() => import('@/components/graphs/Graph'), {
- ssr: false,
-});
-
-interface RateGraphDrawerProps {
- reset: Function;
- resolution: TimeDuration;
- duration: TimeDuration;
- time: DateTime;
- setResolution: Function;
- setDuration: Function;
- setTime: Function;
-}
-
-function LineGraphDrawer({
- reset,
- resolution,
- duration,
- time,
- setResolution,
- setDuration,
- setTime,
-}: RateGraphDrawerProps) {
- return (
-
-
-
-
-
-
-
- );
-}
-
-interface LineGraphProps {
- boxProps?: BoxProps;
- metrics: string[];
- duration?: TimeDuration;
- resolution?: TimeDuration;
- options?: ChartOptions<'line'>;
- datasetOptions?:
- | Partial>
- | Partial>[];
- datasetTransform?: (x: ChartDataset<'line'>) => ChartDataset<'line'>;
-}
-
-async function getData(
- metrics: string[],
- duration: TimeDuration,
- resolution: TimeDuration,
- time: DateTime,
- datasetOptions:
- | Partial>
- | Partial>[],
- datasetTransform?: (x: ChartDataset<'line'>) => ChartDataset<'line'>
-) {
- let chartData: ChartData<'line', any, any> = {
- datasets: await Promise.all(
- metrics.map(async (metric, index) => {
- let datasetOption: Partial> = {};
- if (datasetOptions instanceof Array) {
- try {
- datasetOption = datasetOptions[index];
- } catch (e) {
- console.error(
- 'datasetOptions is an array, but the number of elements < the number of metrics'
- );
- }
- } else {
- datasetOption = datasetOptions;
- }
-
- let updatedTime = time;
- if (updatedTime.hasSame(DateTime.now(), 'day')) {
- updatedTime = DateTime.now();
- }
-
- let dataset = {
- data: await query_basic({
- metric,
- range: duration,
- resolution: resolution,
- time: updatedTime,
- }),
- ...datasetOption,
- };
-
- if (datasetTransform) {
- return datasetTransform(dataset);
- }
-
- return dataset;
- })
- ),
- };
-
- return chartData;
-}
-
-export default function LineGraph({
- boxProps,
- metrics,
- duration = new TimeDuration(31, 'd'),
- resolution = new TimeDuration(1, 'h'),
- options,
- datasetOptions = {},
- datasetTransform,
-}: LineGraphProps) {
- let reset = useCallback(() => {
- setDuration(duration.copy());
- setResolution(resolution.copy());
- setTime(DateTime.now());
- }, [duration, resolution]);
-
- let [_duration, setDuration] = useState(duration);
- let [_resolution, setResolution] = useState(resolution);
- let [_time, setTime] = useState(DateTime.now());
-
- return (
-
- getData(
- metrics,
- _duration,
- _resolution,
- _time,
- datasetOptions,
- datasetTransform
- )
- }
- options={options}
- drawer={
-
- }
- boxProps={boxProps}
- />
- );
-}
diff --git a/web_ui/frontend/components/graphs/OriginMetricPage.tsx b/web_ui/frontend/components/graphs/OriginMetricPage.tsx
new file mode 100644
index 000000000..246fce00d
--- /dev/null
+++ b/web_ui/frontend/components/graphs/OriginMetricPage.tsx
@@ -0,0 +1,181 @@
+import { Box, Grid, Paper, Typography } from '@mui/material';
+import { green, grey, blue } from '@mui/material/colors';
+
+import {
+ ProjectTable,
+ TransferRateGraph,
+ CPUGraph,
+ MemoryGraph,
+ BigBytesMetric,
+ BigMetric,
+ StorageGraph,
+} from '@/components/metrics';
+
+import { buildMetric as bm } from '@/components/graphs/prometheus';
+
+export const OriginMetricPage = ({
+ server_name = undefined,
+}: {
+ server_name?: string;
+}) => {
+ return (
+
+
+
+ {[
+ ,
+ ,
+ ,
+ ,
+ ].map((component, index) => (
+
+ {component}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OriginMetricPage;
diff --git a/web_ui/frontend/components/graphs/RateGraph.tsx b/web_ui/frontend/components/graphs/RateGraph.tsx
deleted file mode 100644
index 6d24994f0..000000000
--- a/web_ui/frontend/components/graphs/RateGraph.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-/***************************************************************
- *
- * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you
- * may not use this file except in compliance with the License. You may
- * obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- ***************************************************************/
-
-'use client';
-
-import dynamic from 'next/dynamic';
-
-import React, { useEffect, useState } from 'react';
-import { ChartOptions, ChartDataset, ChartData } from 'chart.js';
-
-import { DateTime } from 'luxon';
-
-import 'chartjs-adapter-luxon';
-
-import { BoxProps, Grid } from '@mui/material';
-
-import {
- query_rate,
- TimeDuration,
- DurationType,
- PrometheusQuery,
- prometheusResultToDataPoints,
-} from '@/components/graphs/prometheus';
-
-import { GraphDrawer, ResolutionInput, RateInput } from './Drawer';
-
-const Graph = dynamic(() => import('@/components/graphs/Graph'), {
- ssr: false,
-});
-
-interface RateGraphDrawerProps {
- reset: Function;
- rate: TimeDuration;
- resolution: TimeDuration;
- duration: TimeDuration;
- time: DateTime;
- setRate: Function;
- setResolution: Function;
- setDuration: Function;
- setTime: Function;
-}
-
-function RateGraphDrawer({
- reset,
- rate,
- resolution,
- duration,
- time,
- setRate,
- setResolution,
- setDuration,
- setTime,
-}: RateGraphDrawerProps) {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-interface RateGraphProps {
- boxProps?: BoxProps;
- metrics: string[];
- rate?: TimeDuration;
- duration?: TimeDuration;
- resolution?: TimeDuration;
- options?: ChartOptions<'line'>;
- datasetOptions?:
- | Partial>
- | Partial>[];
-}
-
-export default function RateGraph({
- boxProps,
- metrics,
- rate = new TimeDuration(30, 'm'),
- duration = new TimeDuration(1, 'd'),
- resolution = new TimeDuration(1, 'm'),
- options = {},
- datasetOptions = {},
-}: RateGraphProps) {
- let default_rate = rate;
- let default_duration = duration;
- let default_resolution = resolution;
-
- let reset = () => {
- setRate(default_rate.copy());
- setDuration(default_duration.copy());
- setResolution(default_resolution.copy());
- setTime(DateTime.now());
- };
-
- let [_rate, setRate] = useState(rate);
- let [_duration, _setDuration] = useState(duration);
- let [_resolution, setResolution] = useState(resolution);
- let [_time, _setTime] = useState(DateTime.now());
-
- // Create some reasonable defaults for the graph
- let setDuration = (duration: TimeDuration) => {
- if (duration.value == 1) {
- setRate(new TimeDuration(30, 'm'));
- setResolution(new TimeDuration(10, 'm'));
- } else if (duration.value == 7) {
- setRate(new TimeDuration(3, 'h'));
- setResolution(new TimeDuration(30, 'm'));
- } else if (duration.value == 31) {
- setRate(new TimeDuration(12, 'h'));
- setResolution(new TimeDuration(12, 'h'));
- }
-
- _setDuration(duration);
- };
-
- let setTime = (time: DateTime) => {
- // If it's not today, then set time to the end of that day
- // If it's today, then set to date.now
- //
- // This helps us to get the latest data while not going over the wanted time range
- // If we set the time to the future, PromQL will give you random data in the future to
- // interpolate the missing ones
- if (time.hasSame(DateTime.now(), 'day')) {
- time = DateTime.now();
- } else {
- time.set({ hour: 23, minute: 59, second: 59, millisecond: 999 });
- }
- _setTime(time);
- };
-
- async function getData() {
- let chartData: ChartData<'line', any, any> = {
- datasets: await Promise.all(
- metrics.map(async (metric, index) => {
- let datasetOption: Partial> = {};
- if (datasetOptions instanceof Array) {
- try {
- datasetOption = datasetOptions[index];
- } catch (e) {
- console.error(
- 'datasetOptions is an array, but the number of elements < the number of metrics'
- );
- }
- } else {
- datasetOption = datasetOptions;
- }
-
- let updatedTime = _time;
- if (updatedTime.hasSame(DateTime.now(), 'day')) {
- updatedTime = DateTime.now();
- }
-
- const queryResponse = await query_rate({
- metric,
- rate: _rate,
- range: _duration,
- resolution: _resolution,
- time: updatedTime,
- });
-
- const dataPoints = prometheusResultToDataPoints(queryResponse);
-
- return {
- data: dataPoints,
- ...datasetOption,
- };
- })
- ),
- };
-
- return chartData;
- }
-
- return (
-
- }
- options={options}
- boxProps={boxProps}
- />
- );
-}
diff --git a/web_ui/frontend/components/graphs/prometheus.tsx b/web_ui/frontend/components/graphs/prometheus.tsx
index 171675dc0..e62dd9b95 100644
--- a/web_ui/frontend/components/graphs/prometheus.tsx
+++ b/web_ui/frontend/components/graphs/prometheus.tsx
@@ -328,3 +328,44 @@ export const fillArrayNulls = (
return [parseInt(key), value];
});
};
+
+/**
+ * Build a prometheus metric string from a metric name and a set of key value pairs
+ * Results in something like: metric{key1="value1",key2="value2"}
+ */
+export const buildMetric = (
+ metric: string,
+ args: Record<
+ string,
+ string | { comparator: '=' | '!='; value: string } | undefined
+ >
+) => {
+ // Verify the metric name is valid
+ if (!metric.match(/^[a-zA-Z_:][a-zA-Z0-9_:]*$/)) {
+ throw new Error(`Invalid metric name: ${metric}`);
+ }
+
+ // Verify the key names are valid
+ if (
+ !Object.keys(args).every((key) => key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/))
+ ) {
+ throw new Error(`Invalid key name in metric: ${metric}`);
+ }
+
+ const labels = Object.entries(args).reduce((acc: string[], [key, value]) => {
+ if (typeof value === 'string') {
+ acc.push(`${key}="${value}"`);
+ } else if (value === undefined) {
+ } else {
+ acc.push(`${key}${value.comparator}"${value.value}"`);
+ }
+ return acc;
+ }, []);
+
+ let labelString = '';
+ if (labels.length > 0) {
+ labelString = `{${labels.join(',')}}`;
+ }
+
+ return metric + labelString;
+};
diff --git a/web_ui/frontend/components/metrics/CPUGraph.tsx b/web_ui/frontend/components/metrics/CPUGraph.tsx
index 522e3ddfa..48a37f150 100644
--- a/web_ui/frontend/components/metrics/CPUGraph.tsx
+++ b/web_ui/frontend/components/metrics/CPUGraph.tsx
@@ -23,6 +23,7 @@ import {
GraphDispatchContext,
} from '@/components/graphs/GraphContext';
import {
+ buildMetric,
MatrixResponseData,
query_raw,
TimeDuration,
@@ -45,12 +46,13 @@ ChartJS.register(
Filler
);
-const CPUGraph = () => {
+const CPUGraph = ({ server_name = undefined }: { server_name?: string }) => {
const graphContext = useContext(GraphContext);
const { data: datasets } = useSWR>(
[
'cpuGraph',
+ server_name,
graphContext.rate,
graphContext.range,
graphContext.resolution,
@@ -58,6 +60,7 @@ const CPUGraph = () => {
],
() =>
getData(
+ server_name,
graphContext.rate,
graphContext.range,
graphContext.resolution,
@@ -114,12 +117,14 @@ const CPUGraph = () => {
};
const getData = async (
+ server_name: string | undefined,
rate: TimeDuration,
range: TimeDuration,
resolution: TimeDuration,
time: DateTime
): Promise> => {
- const query = `avg by (instance) (irate(process_cpu_seconds_total[${rate}]))[${range}:${resolution}]`;
+ const metric = buildMetric('process_cpu_seconds_total', { server_name });
+ const query = `avg by (instance) (irate(${metric}[${rate}]))[${range}:${resolution}]`;
const dataResponse = await query_raw(
query,
time.toSeconds()
diff --git a/web_ui/frontend/components/metrics/MemoryGraph.tsx b/web_ui/frontend/components/metrics/MemoryGraph.tsx
index 2705e3dc1..fcde713df 100644
--- a/web_ui/frontend/components/metrics/MemoryGraph.tsx
+++ b/web_ui/frontend/components/metrics/MemoryGraph.tsx
@@ -22,6 +22,7 @@ import {
GraphDispatchContext,
} from '@/components/graphs/GraphContext';
import {
+ buildMetric,
MatrixResponseData,
query_raw,
TimeDuration,
@@ -43,7 +44,7 @@ ChartJS.register(
Colors
);
-const MemoryGraph = () => {
+const MemoryGraph = ({ server_name }: { server_name?: string }) => {
const graphContext = useContext(GraphContext);
const { data: datasets } = useSWR>(
@@ -56,6 +57,7 @@ const MemoryGraph = () => {
],
() =>
getData(
+ buildMetric('go_memstats_alloc_bytes', { server_name }),
graphContext.rate,
graphContext.range,
graphContext.resolution,
@@ -112,12 +114,13 @@ const MemoryGraph = () => {
};
const getData = async (
+ metric: string,
rate: TimeDuration,
range: TimeDuration,
resolution: TimeDuration,
time: DateTime
): Promise> => {
- const query = `(go_memstats_alloc_bytes / 1024 / 1024)[${range}:${resolution}]`;
+ const query = `(${metric} / 1024 / 1024)[${range}:${resolution}]`;
const dataResponse = await query_raw(
query,
time.toSeconds()
diff --git a/web_ui/frontend/components/metrics/ProjectsTable.tsx b/web_ui/frontend/components/metrics/ProjectsTable.tsx
index f94511d7d..6b89d59c6 100644
--- a/web_ui/frontend/components/metrics/ProjectsTable.tsx
+++ b/web_ui/frontend/components/metrics/ProjectsTable.tsx
@@ -10,6 +10,7 @@ import {
TableRow,
} from '@mui/material';
import {
+ buildMetric,
MatrixResponseData,
query_raw,
TimeDuration,
@@ -21,56 +22,66 @@ import { useContext } from 'react';
import { GraphContext } from '@/components/graphs/GraphContext';
import { DateTime } from 'luxon';
import { convertToBiggestBytes } from '@/helpers/bytes';
+import StyledTableCell from '@/components/StyledHeadTableCell';
interface ProjectData {
name: string;
bytesAccessed: string;
}
-const ProjectTable = () => {
+const ProjectTable = ({
+ server_name = undefined,
+}: {
+ server_name?: string;
+}) => {
const { rate, time, range, resolution } = useContext(GraphContext);
const { data: projectData, error: projectError } = useSWR(
['projectData', time, range],
- () => getProjectData(range, time)
+ () => getProjectData(server_name, range, time)
);
return (
<>
{projectData !== undefined && (
-
-
-
-
-
- Project
- Bytes Accessed
+
+
+
+
+ Project
+ Bytes Accessed
+
+
+
+ {projectData.map((project) => (
+
+ {project.name}
+
+ {project.bytesAccessed.toLocaleString()}
+
-
-
- {projectData.map((project) => (
-
- {project.name}
-
- {project.bytesAccessed.toLocaleString()}
-
-
- ))}
-
-
-
-
+ ))}
+
+
+
)}
>
);
};
const getProjectData = async (
+ server_name: string | undefined,
range: TimeDuration,
time: DateTime
): Promise => {
+ const metric = buildMetric('xrootd_transfer_bytes', {
+ type: { comparator: '!=', value: 'value' },
+ proj: { comparator: '!=', value: '' },
+ server_name,
+ });
+
const queryResponse = await query_raw(
- `sum by (proj) (increase(xrootd_transfer_bytes{type!="write", proj!=""}[${range}]))`,
+ `sum by (proj) (increase(${metric}[${range}]))`,
time.toSeconds()
);
const result = queryResponse.data.result;
diff --git a/web_ui/frontend/components/metrics/StorageGraph.tsx b/web_ui/frontend/components/metrics/StorageGraph.tsx
index 0128c6eeb..3c9708936 100644
--- a/web_ui/frontend/components/metrics/StorageGraph.tsx
+++ b/web_ui/frontend/components/metrics/StorageGraph.tsx
@@ -15,6 +15,7 @@ import { grey, blue } from '@mui/material/colors';
import { GraphContext } from '@/components/graphs/GraphContext';
import {
+ buildMetric,
MatrixResponseData,
query_raw,
TimeDuration,
@@ -31,12 +32,16 @@ interface storageData {
free: number;
}
-const StorageGraph = () => {
+const StorageGraph = ({
+ server_name = undefined,
+}: {
+ server_name?: string;
+}) => {
const graphContext = useContext(GraphContext);
const { data } = useSWR(
- ['getStorageData', graphContext.time],
- () => getStorageData(graphContext.time.toSeconds())
+ ['getStorageData', graphContext.time, server_name],
+ () => getStorageData(server_name, graphContext.time.toSeconds())
);
const [usedBytes, freeBytes] = convertListBytes([
@@ -74,9 +79,14 @@ const StorageGraph = () => {
};
const getStorageData = async (
+ server_name: string | undefined,
time: number = DateTime.now().toSeconds()
): Promise<{ free: number; used: number }> => {
- const url = `sum by (type) (xrootd_storage_volume_bytes{server_type="origin"})`;
+ const metric = buildMetric('xrootd_storage_volume_bytes', {
+ server_type: 'origin',
+ server_name,
+ });
+ const url = `sum by (type) (${metric})`;
const response = await query_raw(url, time);
const result = response.data.result;
diff --git a/web_ui/frontend/components/metrics/TransferRateGraph.tsx b/web_ui/frontend/components/metrics/TransferRateGraph.tsx
index 5aba26a78..4cbfc6f51 100644
--- a/web_ui/frontend/components/metrics/TransferRateGraph.tsx
+++ b/web_ui/frontend/components/metrics/TransferRateGraph.tsx
@@ -24,6 +24,7 @@ import {
GraphDispatchContext,
} from '@/components/graphs/GraphContext';
import {
+ buildMetric,
MatrixResponseData,
query_raw,
TimeDuration,
@@ -46,7 +47,11 @@ ChartJS.register(
Colors
);
-const TransferRateGraph = () => {
+const TransferRateGraph = ({
+ server_name = undefined,
+}: {
+ server_name?: string;
+}) => {
const graphContext = useContext(GraphContext);
const dispatch = useContext(GraphDispatchContext);
@@ -62,6 +67,7 @@ const TransferRateGraph = () => {
],
() =>
getData(
+ server_name,
graphContext.rate,
graphContext.range,
graphContext.resolution,
@@ -174,12 +180,16 @@ const toBytesDataset = (
};
const getData = async (
+ server_name: string | undefined,
rate: TimeDuration,
range: TimeDuration,
resolution: TimeDuration,
time: DateTime
): Promise[]> => {
- const query = `sum by (path, type) (rate(xrootd_transfer_bytes[${rate}]))[${range}:${resolution}]`;
+ const metric = buildMetric('xrootd_transfer_bytes', {
+ server_name: server_name,
+ });
+ const query = `sum by (path, type) (rate(${metric}[${rate}]))[${range}:${resolution}]`;
const dataResponse = await query_raw(
query,
time.toSeconds()
diff --git a/web_ui/frontend/types.ts b/web_ui/frontend/types.ts
index 01f49554c..4d97f3614 100644
--- a/web_ui/frontend/types.ts
+++ b/web_ui/frontend/types.ts
@@ -29,6 +29,8 @@ export interface DirectorNamespace {
origins: string[];
}
+export type ServerType = 'Origin' | 'Cache';
+
interface ServerBase {
name: string;
version: string;
@@ -38,7 +40,7 @@ interface ServerBase {
brokerUrl: string;
url: string;
webUrl: string;
- type: 'Origin' | 'Cache';
+ type: ServerType;
latitude: number;
longitude: number;
capabilities: Capabilities;