Skip to content

Commit

Permalink
Extract redis stats into modal
Browse files Browse the repository at this point in the history
  • Loading branch information
felixmosh committed Aug 3, 2022
1 parent b74dbb3 commit 51acb2d
Show file tree
Hide file tree
Showing 22 changed files with 459 additions and 259 deletions.
33 changes: 1 addition & 32 deletions packages/api/src/handlers/queues.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { parse as parseRedisInfo } from 'redis-info';
import { BaseAdapter } from '../queueAdapters/base';
import {
AppJob,
AppQueue,
Expand All @@ -10,36 +8,9 @@ import {
Pagination,
QueueJob,
Status,
ValidMetrics,
} from '../../typings/app';
import { STATUSES } from '../constants/statuses';

type MetricName = keyof ValidMetrics;

const metrics: MetricName[] = [
'redis_version',
'used_memory',
'mem_fragmentation_ratio',
'connected_clients',
'blocked_clients',
];

const getStats = async (queue: BaseAdapter): Promise<ValidMetrics> => {
const redisInfoRaw = await queue.getRedisInfo();
const redisInfo = parseRedisInfo(redisInfoRaw);

const validMetrics = metrics.reduce((acc, metric) => {
if (redisInfo[metric]) {
acc[metric] = redisInfo[metric];
}

return acc;
}, {} as Record<MetricName, string>);

validMetrics.total_system_memory = redisInfo.total_system_memory || redisInfo.maxmemory;

return validMetrics;
};
import { BaseAdapter } from '../queueAdapters/base';

const formatJob = (job: QueueJob, queue: BaseAdapter): AppJob => {
const jobProps = job.toJSON();
Expand Down Expand Up @@ -132,11 +103,9 @@ export async function queuesHandler({
const pairs = [...bullBoardQueues.entries()];

const queues = pairs.length > 0 ? await getAppQueues(pairs, query) : [];
const stats = pairs.length > 0 ? await getStats(pairs[0][1]) : {};

return {
body: {
stats,
queues,
},
};
Expand Down
56 changes: 56 additions & 0 deletions packages/api/src/handlers/redisStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { parse as parseRedisInfo } from 'redis-info';
import { BullBoardRequest, ControllerHandlerReturnType, RedisStats } from '../../typings/app';
import { BaseAdapter } from '../queueAdapters/base';

function formatUptime(uptime: number) {
const date = new Date(uptime * 1000);
const days = date.getUTCDate() - 1,
hours = date.getUTCHours(),
minutes = date.getUTCMinutes(),
seconds = date.getUTCSeconds();

// Initialize an array for the uptime.
const segments = [];

// Format the uptime string.
if (days > 0) segments.push(days + ' day' + (days == 1 ? '' : 's'));
if (hours > 0) segments.push(hours + ' hour' + (hours == 1 ? '' : 's'));
if (minutes > 0) segments.push(minutes + ' minute' + (minutes == 1 ? '' : 's'));
if (seconds > 0 && days === 0) segments.push(seconds + ' second' + (seconds == 1 ? '' : 's'));
return segments.join(', ');
}

async function getStats(queue: BaseAdapter): Promise<RedisStats> {
const redisInfoRaw = await queue.getRedisInfo();
const redisInfo = parseRedisInfo(redisInfoRaw);

return {
version: redisInfo.redis_version,
mode: redisInfo.redis_mode,
port: +redisInfo.tcp_port,
os: redisInfo.os,
uptime: formatUptime(+redisInfo.uptime_in_seconds),
memory: {
total: +redisInfo.total_system_memory || +redisInfo.maxmemory,
used: +redisInfo.used_memory,
fragmentationRatio: +redisInfo.mem_fragmentation_ratio,
peak: +redisInfo.used_memory_peak,
},
clients: {
connected: +redisInfo.connected_clients,
blocked: +redisInfo.blocked_clients,
},
};
}

export async function redisStatsHandler({
queues: bullBoardQueues,
}: BullBoardRequest): Promise<ControllerHandlerReturnType> {
const pairs = [...bullBoardQueues.values()];

const body = pairs.length > 0 ? await getStats(pairs[0]) : {};

return {
body,
};
}
2 changes: 2 additions & 0 deletions packages/api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { jobLogsHandler } from './handlers/jobLogs';
import { pauseQueueHandler } from './handlers/pauseQueue';
import { promoteJobHandler } from './handlers/promotJob';
import { queuesHandler } from './handlers/queues';
import { redisStatsHandler } from './handlers/redisStats';
import { resumeQueueHandler } from './handlers/resumeQueue';
import { retryAllHandler } from './handlers/retryAll';
import { retryJobHandler } from './handlers/retryJob';
Expand All @@ -17,6 +18,7 @@ export const appRoutes: AppRouteDefs = {
handler: entryPoint,
},
api: [
{ method: 'get', route: '/api/redis/stats', handler: redisStatsHandler },
{ method: 'get', route: '/api/queues', handler: queuesHandler },
{
method: 'get',
Expand Down
24 changes: 17 additions & 7 deletions packages/api/typings/app.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RedisInfo } from 'redis-info';
import { BaseAdapter } from '../src/queueAdapters/base';
import { STATUSES } from '../src/constants/statuses';

Expand Down Expand Up @@ -50,13 +51,22 @@ export interface QueueJobJson {
parentKey?: string;
}

export interface ValidMetrics {
total_system_memory: string;
redis_version: string;
used_memory: string;
mem_fragmentation_ratio: string;
connected_clients: string;
blocked_clients: string;
export interface RedisStats {
version: string;
mode: RedisInfo['redis_mode'];
port: number;
os: string;
uptime: string;
memory: {
total: number;
used: number;
fragmentationRatio: number;
peak: number;
};
clients: {
connected: number;
blocked: number;
};
}

export interface AppJob {
Expand Down
3 changes: 1 addition & 2 deletions packages/api/typings/responses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AppQueue, ValidMetrics } from './app';
import { AppQueue } from './app';

export interface GetQueuesResponse {
stats: Partial<ValidMetrics>;
queues: AppQueue[];
}
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@radix-ui/react-alert-dialog": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.14",
Expand Down
11 changes: 5 additions & 6 deletions packages/ui/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@ import { ToastContainer } from 'react-toastify';
import { useActiveQueue } from '../hooks/useActiveQueue';
import { useScrollTopOnNav } from '../hooks/useScrollTopOnNav';
import { useStore } from '../hooks/useStore';
import { Api } from '../services/Api';
import { QueueTitle } from './QueueTitle/QueueTitle';
import { ConfirmModal } from './ConfirmModal/ConfirmModal';
import { Header } from './Header/Header';
import { HeaderActions } from './HeaderActions/HeaderActions';
import { Menu } from './Menu/Menu';
import { QueuePage } from './QueuePage/QueuePage';
import { RedisStats } from './RedisStats/RedisStats';
import { QueueTitle } from './QueueTitle/QueueTitle';

export const App = ({ api }: { api: Api }) => {
export const App = () => {
useScrollTopOnNav();
const { state, actions, selectedStatuses, confirmProps } = useStore(api);
const { state, actions, selectedStatuses, confirmProps } = useStore();
const activeQueue = useActiveQueue(state.data);

return (
<>
<Header>
{!!activeQueue && <QueueTitle queue={activeQueue} />}
{state.data?.stats && <RedisStats stats={state.data?.stats} />}
<HeaderActions />
</Header>
<main>
<div>
Expand Down
97 changes: 0 additions & 97 deletions packages/ui/src/components/ConfirmModal/ConfirmModal.module.css
Original file line number Diff line number Diff line change
@@ -1,100 +1,3 @@
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

@keyframes slideUp {
from {
transform: translateY(16px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

@keyframes slideDown {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(16px);
opacity: 0;
}
}

.overlay[data-state='open'] {
animation: fadeIn 150ms ease-out;
}

.overlay[data-state='closed'] {
animation: fadeOut 150ms ease-in 150ms;
}

.contentWrapper[data-state='open'] {
animation: slideUp 150ms ease-out;
}

.contentWrapper[data-state='closed'] {
animation: slideDown 150ms ease-in;
}

.overlay {
position: fixed;
top: 0 !important;
left: 0 !important;
width: 100%;
height: 100%;
text-align: center;
vertical-align: middle;
padding: 1em;
background-color: rgba(0, 0, 0, 0.85);
user-select: none;
will-change: opacity;
z-index: 1000;
}

.contentWrapper {
padding: 1rem;
position: fixed;
top: 0 !important;
left: 0 !important;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1100;
}

.content {
max-width: 450px;
width: 100%;
background-color: white;
border-radius: 0.5rem;
padding: 1rem;
}

.actions {
background: #f9fafb;
margin: 1rem -1rem -1rem;
border-radius: 0 0 0.5rem 0.5rem;
padding: 1rem 1rem;
text-align: right;
}
26 changes: 14 additions & 12 deletions packages/ui/src/components/ConfirmModal/ConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
Title,
Portal,
} from '@radix-ui/react-alert-dialog';
import cn from 'clsx';
import React from 'react';
import s from './ConfirmModal.module.css';
import modalStyles from '../Modal/Modal.module.css';
import { Button } from '../JobCard/Button/Button';

export interface ConfirmProps {
Expand All @@ -20,33 +22,33 @@ export interface ConfirmProps {
onConfirm: () => void;
}

export const ConfirmModal = (props: ConfirmProps) => {
export const ConfirmModal = ({ open, onConfirm, title, onCancel, description }: ConfirmProps) => {
const closeOnOpenChange = (open: boolean) => {
if (!open) {
props.onCancel();
onCancel();
}
};

return (
<Root open={props.open} onOpenChange={closeOnOpenChange}>
<Root open={open} onOpenChange={closeOnOpenChange}>
<Portal>
<Overlay className={s.overlay} />
<Content className={s.contentWrapper}>
<div className={s.content}>
{!!props.title && (
<Overlay className={modalStyles.overlay} />
<Content className={modalStyles.contentWrapper}>
<div className={cn(modalStyles.content, s.content)}>
{!!title && (
<Title asChild>
<h3>{props.title}</h3>
<h3>{title}</h3>
</Title>
)}
{!!props.description && <Description>{props.description}</Description>}
<div className={s.actions}>
{!!description && <Description>{description}</Description>}
<div className={modalStyles.actions}>
<Action asChild>
<Button theme="primary" onClick={props.onConfirm}>
<Button theme="primary" onClick={onConfirm}>
Confirm
</Button>
</Action>
<Cancel asChild>
<Button theme="basic" onClick={props.onCancel}>
<Button theme="basic" onClick={onCancel}>
Cancel
</Button>
</Cancel>
Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/components/HeaderActions/HeaderActions.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.actions {
padding: 0;
margin: 0;
list-style: none;
}

.button {
font-size: 1rem;
padding: 0.65rem;
}

.button > svg {
width: 1.5rem;
}
Loading

0 comments on commit 51acb2d

Please sign in to comment.