Skip to content

Commit

Permalink
task/TUP-331 -- system monitor client (#50)
Browse files Browse the repository at this point in the history
* useSystemMonitor hook

* useSystemMonitor hook

* sysmon should use hook results

* fix system monitor hook not returning values

* clean up system monitor data structure

* Convert sysmon to typescript

* System monitor tests

* Comment about alternate baseUrl

* linting

* Use more idiomatic types for table cells

* Set correct URLs for profile and sysmon

* Set local dev tup-services url to http://localhost:8001

* Fix system monitor operational status test

* Truncate load percentage

* formatting

Co-authored-by: jarosenb <jrosenberg@tacc.utexas.edu>
  • Loading branch information
jchuahtacc and jarosenb authored Sep 8, 2022
1 parent ecdd9f4 commit 3d8fb3c
Show file tree
Hide file tree
Showing 19 changed files with 592 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('LoginComponent', () => {
it('should display an error message on bad passwords', async () => {
// Mock a 403 error
server.use(
rest.post('http://localhost:8000/auth', (req, res, ctx) => {
rest.post('http://localhost:8001/auth', (req, res, ctx) => {
return res.once(
ctx.status(403),
ctx.json({ message: 'Invalid username/password' })
Expand Down Expand Up @@ -71,7 +71,7 @@ describe('LoginComponent', () => {
it('should display an error message for other status codes', async () => {
// Mock a 403 error
server.use(
rest.post('http://localhost:8000/auth', (req, res, ctx) => {
rest.post('http://localhost:8001/auth', (req, res, ctx) => {
return res.once(
ctx.status(500),
ctx.json({ message: 'Internal server error' })
Expand Down
77 changes: 77 additions & 0 deletions apps/tup-ui/src/components/sysmon/SystemMonitor.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.root {
--table-border: 1px solid black;

margin: 0;
width: 100%;

font-size: 0.875rem; /* 14px (approved deviation from design) */
}

.rows tr:nth-child(even) {
background-color: rgb(0 0 0 / 3%);
}
.rows tr:hover {
background-color: rgb(0 0 0 / 5%);
}
.rows td {
padding: 5px 8px;
}

.header {
border-bottom: var(--table-border);
}

/* Columns */
@media only screen and (min-width: 1200px) {
/* name */
.root tr > *:nth-child(1) {
width: 22%;
}
/* status */
.root tr > *:nth-child(2) {
width: 30%;
}
/* load */
.root tr > *:nth-child(3) {
width: 16%;
}
/* run */
.root tr > *:nth-child(4) {
width: 16%;
}
/* queue */
.root tr > *:nth-child(5) {
width: 16%;
}
}
@media only screen and (max-width: 1199px) {
/* name */
.root tr > *:nth-child(1) {
width: 22%;
}
/* status */
.root tr > *:nth-child(2) {
width: 24%;
}
/* load */
.root tr > *:nth-child(3) {
width: 18%;
}
/* run */
.root tr > *:nth-child(4) {
width: 18%;
}
/* queue */
.root tr > *:nth-child(5) {
width: 18%;
}
}

/* Messaging */
.error {
display: flex;
justify-content: center;
align-items: center;
color: var(--global-color-accent--normal);
padding: 30px;
}
17 changes: 17 additions & 0 deletions apps/tup-ui/src/components/sysmon/SystemMonitor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import SystemMonitor from './SystemMonitor';
import { testRender } from '../../utils';
import { waitFor } from '@testing-library/react';

describe('System Monitor Component', () => {
it('display a no-systems message when there is no data', async () => {
const { getByText } = testRender(<SystemMonitor hosts={[]} />);
await waitFor(() =>
expect(getByText('No systems being monitored')).toBeDefined()
);
});
it('should display the system name in each row', async () => {
const { getByText } = testRender(<SystemMonitor />);
await waitFor(() => expect(getByText('Frontera')).toBeDefined());
});
});
97 changes: 97 additions & 0 deletions apps/tup-ui/src/components/sysmon/SystemMonitor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useMemo } from 'react';
import { useTable, Column } from 'react-table';
import { LoadingSpinner, Message } from '@tacc/core-components';
import { Display, Operational, Load } from './SystemMonitorCells';
import { SystemMonitorSystem, useSystemMonitor } from '../../hooks';
import styles from './SystemMonitor.module.css';

const SystemMonitor: React.FC<{ hosts?: Array<string> }> = ({ hosts }) => {
const { systems, isLoading, error } = useSystemMonitor(hosts);
const columns = useMemo<Column<SystemMonitorSystem>[]>(
() => [
{
accessor: 'display_name',
Header: 'Name',
Cell: Display,
},
{
accessor: 'isOperational',
Header: 'Status',
Cell: Operational,
},
{
accessor: 'loadPercentage',
Header: 'Load',
Cell: Load,
},
{
accessor: ({ jobs }) => (jobs ? jobs.running : '--'),
Header: 'Running',
},
{
accessor: ({ jobs }) => (jobs ? jobs.queued : '--'),
Header: 'Queued',
},
],
[]
);
const { getTableProps, getTableBodyProps, rows, prepareRow, headerGroups } =
useTable({
columns,
data: systems,
});

if (isLoading) {
return <LoadingSpinner />;
}

if (error) {
return (
<Message type="warn" className={styles['error']}>
Unable to gather system information
</Message>
);
}

return (
<table
{...getTableProps()}
// Emulate <InfiniteScrollTable> and its use of `o-fixed-header-table`
// TODO: Create global table styles & Make <InfiniteScrollTable> use them
className={`multi-system InfiniteScrollTable o-fixed-header-table ${styles['root']}`}
>
<thead>
{headerGroups.map((headerGroup) => (
<tr
{...headerGroup.getHeaderGroupProps()}
className={styles['header']}
>
{headerGroup.headers.map((column) => (
<th key={column.id}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()} className={styles['rows']}>
{rows.length ? (
rows.map((row, idx) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
);
})
) : (
<tr>
<td colSpan={5}>No systems being monitored</td>
</tr>
)}
</tbody>
</table>
);
};

export default SystemMonitor;
21 changes: 21 additions & 0 deletions apps/tup-ui/src/components/sysmon/SystemMonitorCells.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { Pill } from '@tacc/core-components';
import { Cell } from 'react-table';
import { SystemMonitorSystem } from '../../hooks';

export const Display: React.FC<{ cell: Cell<SystemMonitorSystem, string> }> = ({
cell: { value },
}) => <strong className="wb-text-primary">{value}</strong>;

export const Operational: React.FC<{
cell: Cell<SystemMonitorSystem, boolean>;
}> = ({ cell: { value } }) => {
if (value) {
return <Pill type="success">Operational</Pill>;
}
return <Pill type="warning">Maintenance</Pill>;
};

export const Load: React.FC<{
cell: Cell<SystemMonitorSystem, number | undefined>;
}> = ({ cell: { value } }) => <span>{value ? `${value}%` : '--'}</span>;
1 change: 1 addition & 0 deletions apps/tup-ui/src/components/sysmon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SystemMonitor';
43 changes: 43 additions & 0 deletions apps/tup-ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,49 @@ export type AuthBody = {
password: string;
};

export type SystemMonitorTest = {
type: string;
status: boolean;
timestamp: string;
};

export type SystemMonitorRawSystem = {
hostname: string;
displayName: string;
ssh?: SystemMonitorTest;
tests?: {
heartbeat?: SystemMonitorTest;
ssh?: SystemMonitorTest;
};
timestamp: string;
jobs?: {
running: number;
queued: number;
other: number;
};
totalCpu: number;
usedCpu: number;
load: number;
heartbeat?: SystemMonitorTest;
};

export type SystemMonitorRaw = {
[hostname: string]: SystemMonitorRawSystem;
};

export type SystemMonitorSystem = {
hostname: string;
display_name: string;
isOperational: boolean;
loadPercentage?: number;
jobs?: {
running: number;
queued: number;
other: number;
};
};

export { default as useAuth } from './useAuth';
export { default as useProfile } from './useProfile';
export { default as useJwt } from './useJwt';
export { default as useSystemMonitor } from './useSystemMonitor';
22 changes: 14 additions & 8 deletions apps/tup-ui/src/hooks/requests.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ describe('requests', () => {

jest.spyOn(axios, 'get').mockResolvedValue({ data: 'response' });

const { result } = renderHook(() => useGet<string>('/endpoint', 'key'), {
wrapper: TestWrapper,
});
const { result } = renderHook(
() => useGet<string>({ endpoint: '/endpoint', key: 'key' }),
{
wrapper: TestWrapper,
}
);
await waitFor(() => expect(result.current.data).toEqual('response'));
expect(axios.get).toHaveBeenCalledWith('http://localhost:8000/endpoint', {
expect(axios.get).toHaveBeenCalledWith('http://localhost:8001/endpoint', {
headers: { 'x-tup-token': 'abc123' },
});
});
Expand All @@ -38,13 +41,16 @@ describe('requests', () => {
(axios.post as jest.Mock).mockResolvedValue({
data: 'response',
});
const { result } = renderHook(() => usePost<string, string>('/endpoint'), {
wrapper: TestWrapper,
});
const { result } = renderHook(
() => usePost<string, string>({ endpoint: '/endpoint' }),
{
wrapper: TestWrapper,
}
);
act(() => result.current.mutate('body'));
await waitFor(() => expect(result.current.data).toEqual('response'));
expect(axios.post).toHaveBeenCalledWith(
'http://localhost:8000/endpoint',
'http://localhost:8001/endpoint',
'body',
{ headers: { 'x-tup-token': 'abc123' } }
);
Expand Down
47 changes: 31 additions & 16 deletions apps/tup-ui/src/hooks/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,51 @@ import {
UseMutationOptions,
} from 'react-query';

export function useGet<ResponseType>(
endpoint: string,
key: string,
options: Omit<
UseQueryOptions<ResponseType, Error>,
'queryKey' | 'queryFn'
> = {}
) {
type UseGetParams<ResponseType> = {
endpoint: string;
key: string;
options?: Omit<UseQueryOptions<ResponseType, Error>, 'queryKey' | 'queryFn'>;
baseUrl?: string;
};

export function useGet<ResponseType>({
endpoint,
key,
options = {},
baseUrl: alternateBaseUrl,
}: UseGetParams<ResponseType>) {
const client = axios;
const { baseUrl } = useConfig();
const { jwt } = useJwt();
const getUtil = async () => {
const request = await client.get<ResponseType>(`${baseUrl}${endpoint}`, {
headers: { 'x-tup-token': jwt ?? '' },
});
const request = await client.get<ResponseType>(
`${alternateBaseUrl ?? baseUrl}${endpoint}`,
{
headers: { 'x-tup-token': jwt ?? '' },
}
);
return request.data;
};
return useQuery(key, () => getUtil(), options);
}

export function usePost<BodyType, ResponseType>(
endpoint: string,
options: UseMutationOptions<ResponseType, Error, BodyType> = {}
) {
type UsePostParams<BodyType, ResponseType> = {
endpoint: string;
options?: UseMutationOptions<ResponseType, Error, BodyType>;
baseUrl?: string;
};

export function usePost<BodyType, ResponseType>({
endpoint,
options = {},
baseUrl: alternateBaseUrl,
}: UsePostParams<BodyType, ResponseType>) {
const client = axios;
const { baseUrl } = useConfig();
const { jwt } = useJwt();
const postUtil = async (body: BodyType) => {
const response = await client.post<ResponseType>(
`${baseUrl}${endpoint}`,
`${alternateBaseUrl ?? baseUrl}${endpoint}`,
body,
{
headers: { 'x-tup-token': jwt ?? '' },
Expand Down
Loading

0 comments on commit 3d8fb3c

Please sign in to comment.