Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import withMockdate from '@netsells/storybook-mockdate';
import { ReqoreContent, ReqoreLayoutContent, ReqoreUIProvider } from '@qoretechnologies/reqore';
import { Client, Server } from 'mock-socket';
import { initializeReqraft } from '../src';

export const parameters = {
mockAddonConfigs: {
ignoreQueryParams: true,
globalMockData: [],
},
actions: { argTypesRegex: '^on[A-Z].*' },
layout: 'fullscreen',
options: {
Expand Down Expand Up @@ -39,6 +44,26 @@ export const argTypes = {
},
};

export let ApiEventsWebSocket: Client;

const url = `wss://hq.qoretechnologies.com:8092/apievents?token=${process.env.REACT_APP_QORUS_TOKEN}`;
try {
let server = new Server(url);

server.on('connection', (socket) => {
ApiEventsWebSocket = socket;

socket.on('message', (data) => {
if (data === 'ping') {
socket.send('pong');
return;
}
});
});
} catch (e) {
console.warn('WebSocket server threw:', e.message);
}

export const decorators = [
withMockdate,
(Story, context) => {
Expand Down
80 changes: 80 additions & 0 deletions __tests__/services/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { QorusService } from '@qoretechnologies/ts-toolkit';
import { size } from 'lodash';
import { ApiEventsWebSocket } from '../../.storybook/preview';
import { QorusServiceEnableCallResponse } from '../../src/features/services/api';
import { QorusServiceEnableEventInfo } from '../../src/features/services/events';
import { MockServicesData } from './data';

export const GetServices = {
url: 'https://hq.qoretechnologies.com:8092/api/latest/services/',
method: 'GET',
status: 200,
response: MockServicesData,
};

export const ToggleEnableServices = {
url: 'https://hq.qoretechnologies.com:8092/api/latest/services/',
method: 'PUT',
status: 200,
response: (request) => {
const { searchParams, body } = request;
const { ids } = JSON.parse(body);
const affectedServices = MockServicesData.filter((service) => ids.includes(service.serviceid));
const action = searchParams.action;

const getResponseData = (service: QorusService) => {
switch (action) {
case 'enable':
return {
info: size(service.alerts)
? `Service ${service.serviceid} was NOT enabled`
: `Service ${service.serviceid} enabled`,
enabled: size(service.alerts) ? false : true,
} satisfies Partial<QorusServiceEnableCallResponse>;
case 'disable':
return {
info: `Service ${service.serviceid} was disabled`,
disabled: true,
} satisfies Partial<QorusServiceEnableCallResponse>;
}
};

let responseEvents: unknown;
let result: unknown;

switch (action) {
case 'enable':
case 'disable': {
responseEvents = affectedServices
.filter((service) => (action === 'enable' ? !size(service.alerts) : true))
.map((service) => ({
eventstr: 'GROUP_STATUS_CHANGED',
info: {
id: service.serviceid,
enabled: action === 'enable' ? (size(service.alerts) ? false : true) : false,
name: service.name,
synthetic: false,
type: 'service',
},
})) satisfies Partial<QorusServiceEnableEventInfo>[];

result = affectedServices.map((service) => ({
arg: 'any',
serviceid: service.serviceid,
name: service.name,
type: service.type,
version: service.version,
...getResponseData(service),
})) satisfies QorusServiceEnableCallResponse[];

break;
}
}

// Send events to the WebSocket
ApiEventsWebSocket.send(JSON.stringify(responseEvents));

// Return the result (this is a 200 response with the data)
return result;
},
};
91 changes: 91 additions & 0 deletions __tests__/services/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { QorusService } from '@qoretechnologies/ts-toolkit';

export const MockServicesData = [
{
autostart: true,
enabled: true,
loaded: '2023-10-03T12:00:00Z',
name: 'Enabled_Service',
display_name: 'Enabled Service',
serviceid: 1,
type: 'user',
version: '1.0',
manual_autostart: false,
methods: [],
remote: false,
stateless: false,
status: 'whatever',
},
{
autostart: false,
enabled: false,
name: 'Disabled_Service',
display_name: 'Disabled Service',
serviceid: 2,
type: 'user',
version: '1.0',
manual_autostart: false,
methods: [],
remote: false,
stateless: false,
status: 'whatever',
},
{
autostart: false,
enabled: false,
name: 'service_3',
alerts: [
{
alert: 'alert_1',
alertid: 1,
alerttype: 'ONGOING',
id: 3,
instance: 'mock_instance',
local: false,
name: 'Alert 1',
object: 'service_3',
reason: 'This is a test',
source: 'service_3',
type: 'SERVICE',
when: '2023-10-03T12:00:00Z',
who: 'mock_user',
},
],
display_name: 'Service With Alerts',
serviceid: 3,
type: 'user',
version: '1.0',
manual_autostart: false,
methods: [],
remote: false,
stateless: false,
status: 'whatever',
},
{
autostart: false,
enabled: true,
name: 'service_4',
display_name: 'Service With No Alerts',
serviceid: 4,
type: 'user',
version: '1.0',
manual_autostart: false,
methods: [],
remote: true,
stateless: false,
status: 'whatever',
},
{
autostart: true,
enabled: true,
name: 'service_5',
display_name: 'System Service',
serviceid: 5,
type: 'system',
version: '1.0',
manual_autostart: false,
methods: [],
stateless: false,
status: 'whatever',
},
] as QorusService[];
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@qoretechnologies/reqraft",
"version": "0.7.4",
"version": "0.8.0",
"description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -17,6 +17,7 @@
"start": "yarn storybook",
"storybook": "storybook dev -p 6008",
"update-reqore": "yarn add -D @qoretechnologies/reqore@beta",
"update-toolkit": "yarn add -D @qoretechnologies/ts-toolkit@beta",
"test-storybook": "DEBUG_PRINT_LIMIT=300 test-storybook --url http://localhost:6008",
"install-playwright": "npx playwright@latest install --with-deps",
"build-storybook": "storybook build",
Expand Down Expand Up @@ -54,7 +55,8 @@
"@babel/preset-typescript": "^7.12.7",
"@chromatic-com/storybook": "^2.0.2",
"@netsells/storybook-mockdate": "^0.3.3",
"@qoretechnologies/reqore": "^0.52.3",
"@qoretechnologies/reqore": "^0.53.12",
"@qoretechnologies/ts-toolkit": "^0.5.31",
"@storybook/addon-actions": "^8.3.5",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
Expand Down
41 changes: 41 additions & 0 deletions src/features/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { IReqraftQueryConfig, isError, query } from '../utils/fetch';
import { FEATURES_API_URL } from './constants';

export interface QorusFeatureLoadOptions<T> extends Partial<IReqraftQueryConfig<T>> {
type?: keyof typeof FEATURES_API_URL;
}

export interface QorusFeatureEnableOptions<T> extends QorusFeatureLoadOptions<T> {
id?: string | number;
enable?: boolean;
}

export const load = async <T>({ type, ...options }: QorusFeatureLoadOptions<T>) => {
const result = await query<T>({ ...options, url: FEATURES_API_URL[type], cache: false });

if (isError(result)) {
return Promise.reject(result);
}

return result;
};

export const toggleEnabled = async <T>({
type,
id,
enable,
...options
}: QorusFeatureEnableOptions<T>) => {
const result = await query<T>({
...options,
method: 'PUT',
url: `${FEATURES_API_URL[type]}/${id}?action=${enable ? 'enable' : 'disable'}`,
cache: false,
});

if (isError(result)) {
return Promise.reject(result);
}

return result;
};
29 changes: 29 additions & 0 deletions src/features/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
import { QOGS_API_URL } from './qogs/constants';
import { SERVICES_API_URL } from './services/constants';

export interface QorusFeatureStore<T> {
loading: boolean;
data: (T & { lastUpdated?: number })[];
error?: Error;
errorData?: string;
load: () => Promise<T>;

itemById: (id: string | number) => T | undefined;
idKey?: string;
updateItem: (id: string | number, data: Partial<T>) => void;

registerApiEvents?: () => void;
hasRegisteredApiEvents?: boolean;

hasPermissions: (permissions: string[]) => boolean;
}

export const FEATURES_API_URL = {
qogs: QOGS_API_URL,
services: SERVICES_API_URL,
};

export const FEATURES_ICONS: Record<string, IReqoreIconName> = {
services: 'ServerLine',
};
34 changes: 34 additions & 0 deletions src/features/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { QorusAlert } from '@qoretechnologies/ts-toolkit';
import { QorusServiceApiEvent } from './services/events';

export interface QorusBaseApiEvent {
class: number;
classstr: string;
compositeseverity: number;
compositeseveritystr: string;
event: number;
id: number;
severity: number;
severitystr: string;
time: string;
timeus: number;
}

export interface QorusGlobalAlertRaisedEvent extends QorusBaseApiEvent {
eventstr: typeof QorusGlobalEvents.AlertRaised;
info: QorusAlert;
}

export interface QorusGlobalAlertClearedEvent extends QorusBaseApiEvent {
eventstr: typeof QorusGlobalEvents.AlertCleared;
info: QorusAlert;
}

export type QorusAlertApiEvent = QorusGlobalAlertRaisedEvent | QorusGlobalAlertClearedEvent;

export type QorusApiEvent = QorusServiceApiEvent | QorusAlertApiEvent;

export const QorusGlobalEvents = {
AlertRaised: 'ALERT_ONGOING_RAISED',
AlertCleared: 'ALERT_ONGOING_CLEARED',
} as const;
40 changes: 40 additions & 0 deletions src/features/qogs/Qogs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ReqoreMessage, ReqorePanel, ReqoreSpinner, ReqoreTree } from '@qoretechnologies/reqore';
import { StoryObj } from '@storybook/react';
import { fireEvent } from '@storybook/test';
import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils';
import { StoryMeta } from '../../types';
import { QorusQogsStore } from './QogsStore';

const meta = {
title: 'Features/Qogs',
render: () => {
const { load, loading, data, error, errorData } = QorusQogsStore();

return loading ? (
<ReqoreSpinner />
) : (
<ReqorePanel bottomActions={[{ label: 'Refetch', onClick: load }]} fill>
{error ? (
<ReqoreMessage intent='danger' title='Error loading Qogs' opaque={false}>
{errorData}
</ReqoreMessage>
) : (
<ReqoreTree data={data} fill />
)}
</ReqorePanel>
);
},
} as StoryMeta<any>;

export default meta;
export type Story = StoryObj<typeof meta>;

export const QogsCanBeLoaded: Story = {
play: async () => {
await testsClickButton({ label: 'Refetch' });
await testsWaitForText('0:');
await sleep(1000);
await fireEvent.click(document.querySelector('.reqore-tree-toggle') as HTMLElement);
await testsWaitForText('"fsm3"');
},
};
Loading
Loading