Skip to content

Commit

Permalink
feat: implement info panel
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowusr committed Dec 6, 2024
1 parent 1277bef commit 9dca0e8
Show file tree
Hide file tree
Showing 23 changed files with 397 additions and 65 deletions.
4 changes: 3 additions & 1 deletion lib/gui/constants/client-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export const ClientEvents = {
RETRY: 'retry',
ERROR: 'err',

END: 'end'
END: 'end',

CONNECTED: 'connected'
} as const;

export type ClientEvents = typeof ClientEvents;
Expand Down
5 changes: 5 additions & 0 deletions lib/gui/event-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Response} from 'express';
import stringify from 'json-stringify-safe';
import {ClientEvents} from './constants';

export class EventSource {
private _connections: Response[];
Expand All @@ -9,6 +10,10 @@ export class EventSource {

addConnection(connection: Response): void {
this._connections.push(connection);

connection.write('event: ' + ClientEvents.CONNECTED + '\n');
connection.write('data: 1\n');
connection.write('\n\n');
}

emit(event: string, data?: unknown): void {
Expand Down
3 changes: 2 additions & 1 deletion lib/static/modules/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ export default {
UPDATE_LOADING_TITLE: 'UPDATE_LOADING_TITLE',
UPDATE_LOADING_IS_IN_PROGRESS: 'UPDATE_LOADING_IS_IN_PROGRESS',
SELECT_ALL: 'SELECT_ALL',
DESELECT_ALL: 'DESELECT_ALL'
DESELECT_ALL: 'DESELECT_ALL',
SET_GUI_SERVER_CONNECTION_STATUS: 'SET_GUI_SERVER_CONNECTION_STATUS'
} as const;
11 changes: 11 additions & 0 deletions lib/static/modules/actions/gui-server-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Action} from '@/static/modules/actions/types';
import actionNames from '@/static/modules/action-names';

type SetGuiServerConnectionStatusAction = Action<typeof actionNames.SET_GUI_SERVER_CONNECTION_STATUS, {
isConnected: boolean;
wasDisconnected?: boolean;
}>;
export const setGuiServerConnectionStatus = (payload: SetGuiServerConnectionStatusAction['payload']): SetGuiServerConnectionStatusAction =>
({type: actionNames.SET_GUI_SERVER_CONNECTION_STATUS, payload});

export type GuiServerConnectionAction = SetGuiServerConnectionStatusAction;
4 changes: 3 additions & 1 deletion lib/static/modules/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {ThunkAction} from 'redux-thunk';
import {State} from '@/static/new-ui/types/store';
import {LifecycleAction} from '@/static/modules/actions/lifecycle';
import {SuitesPageAction} from '@/static/modules/actions/suites-page';
import {GuiServerConnectionAction} from '@/static/modules/actions/gui-server-connection';

export type {Dispatch} from 'redux';

Expand All @@ -22,4 +23,5 @@ export type AppThunk<ReturnType = Promise<void>> = ThunkAction<ReturnType, State
export type SomeAction =
| GroupTestsAction
| LifecycleAction
| SuitesPageAction;
| SuitesPageAction
| GuiServerConnectionAction;
7 changes: 6 additions & 1 deletion lib/static/modules/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export default Object.assign({config: configDefaults}, {
availableSections: [],
availableExpressions: [],
currentExpressionIds: []
},
guiServerConnection: {
isConnected: false,
wasDisconnected: false
}
},
ui: {
Expand All @@ -133,5 +137,6 @@ export default Object.assign({config: configDefaults}, {
staticImageAccepterToolbar: {
offset: {x: 0, y: 0}
}
}
},
timestamp: 0
}) satisfies State;
21 changes: 21 additions & 0 deletions lib/static/modules/reducers/gui-server-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {State} from '@/static/new-ui/types/store';
import {SomeAction} from '@/static/modules/actions/types';
import actionNames from '@/static/modules/action-names';
import {applyStateUpdate} from '@/static/modules/utils';

export default (state: State, action: SomeAction): State => {
switch (action.type) {
case actionNames.SET_GUI_SERVER_CONNECTION_STATUS: {
return applyStateUpdate(state, {
app: {
guiServerConnection: {
isConnected: action.payload.isConnected,
wasDisconnected: action.payload.wasDisconnected
}
}
});
}
default:
return state;
}
};
4 changes: 3 additions & 1 deletion lib/static/modules/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import suitesPage from './suites-page';
import visualChecksPage from './visual-checks-page';
import isInitialized from './is-initialized';
import newUiGroupedTests from './new-ui-grouped-tests';
import guiServerConnection from './gui-server-connection';

// The order of specifying reducers is important.
// At the top specify reducers that does not depend on other state fields.
Expand Down Expand Up @@ -64,5 +65,6 @@ export default reduceReducers(
progressBar,
suitesPage,
visualChecksPage,
isInitialized
isInitialized,
guiServerConnection
);
16 changes: 16 additions & 0 deletions lib/static/new-ui/app/gui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {ClientEvents} from '@/gui/constants';
import {App} from './App';
import store from '../../modules/store';
import {finGuiReport, thunkInitGuiReport, suiteBegin, testBegin, testResult, testsEnd} from '../../modules/actions';
import {setGuiServerConnectionStatus} from '@/static/modules/actions/gui-server-connection';
import actionNames from '@/static/modules/action-names';

const rootEl = document.getElementById('app') as HTMLDivElement;
const root = createRoot(rootEl);
Expand All @@ -13,6 +15,20 @@ function Gui(): ReactNode {
const subscribeToEvents = (): void => {
const eventSource = new EventSource('/events');

eventSource.addEventListener(ClientEvents.CONNECTED, (): void => {
store.dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: false});

store.dispatch(setGuiServerConnectionStatus({isConnected: true}));
});

eventSource.onerror = (): void => {
store.dispatch({type: actionNames.UPDATE_LOADING_IS_IN_PROGRESS, payload: true});
store.dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: 'Lost connection to Testplane UI server. Trying to reconnect'});
store.dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: true});

store.dispatch(setGuiServerConnectionStatus({isConnected: false, wasDisconnected: true}));
};

eventSource.addEventListener(ClientEvents.BEGIN_SUITE, (e) => {
const data = JSON.parse(e.data);
store.dispatch(suiteBegin(data));
Expand Down
14 changes: 14 additions & 0 deletions lib/static/new-ui/components/AsidePanel/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.container {
background-color: #fff;
padding: 20px;
width: 440px;
height: 100%;
overflow-x: scroll;

--g-text-body-font-weight: 450;
--g-text-body-short-font-size: 15px;
}

.divider {
margin: 12px 0 20px 0;
}
19 changes: 19 additions & 0 deletions lib/static/new-ui/components/AsidePanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Divider} from '@gravity-ui/uikit';
import classNames from 'classnames';
import React, {ReactNode} from 'react';

import styles from './index.module.css';

interface AsidePanelProps {
title: string;
children?: ReactNode;
className?: string;
}

export function AsidePanel(props: AsidePanelProps): ReactNode {
return <div className={classNames(styles.container, props.className)}>
<h2 className={classNames('text-display-1')}>{props.title}</h2>
<Divider className={styles.divider} orientation={'horizontal'} />
{props.children}
</div>;
}
66 changes: 66 additions & 0 deletions lib/static/new-ui/components/InfoPanel/DataSourceItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.data-source-item {
display: flex;
justify-content: space-between;
padding: 12px 4px;
gap: 28px;
}

.data-source-title-container {
display: flex;
gap: 8px;
overflow: hidden;
}

.data-source-icon {
flex-shrink: 0;
color: var(--g-color-private-black-450-solid);
}

.data-source-status {
font-family: monospace;
display: flex;
align-items: center;
gap: 8px;
}

.data-source-status-circle {
width: 6px;
height: 6px;
border-radius: 100vh;
}

.data-source-status-circle-offline {
box-shadow: 0 0 0 4px var(--g-color-private-red-50);
background-color: var(--g-color-private-red-600-solid);
}

@keyframes pulse {
0% {
box-shadow: 0 0 0 0 var(--pulse-color-from);
}

100% {
box-shadow: 0 0 0 4px var(--pulse-color-to);
}
}

.data-source-status-circle-online {
--pulse-color-from: var(--g-color-private-green-500);
--pulse-color-to: rgba(255 255 255 / 0);
animation: pulse 2s ease infinite;
background-color: var(--g-color-private-green-550-solid);
}

.title {
white-space: nowrap;
display: inline-block;
}

.ellipsis {
display: inline-block;
vertical-align: bottom;
white-space: nowrap;
max-width: var(--max-width);
overflow: hidden;
text-overflow: ellipsis;
}
34 changes: 34 additions & 0 deletions lib/static/new-ui/components/InfoPanel/DataSourceItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Icon, IconProps} from '@gravity-ui/uikit';
import classNames from 'classnames';
import React, {ReactNode} from 'react';

import styles from './DataSourceItem.module.css';

interface DataSourceItemProps {
icon: IconProps['data'];
title: string;
success: boolean;
statusCode?: number | string;
url?: string;
}

export function DataSourceItem(props: DataSourceItemProps): ReactNode {
const urlTitle = props.url?.replace(/^https?:\/\//, '');

return <div className={styles.dataSourceItem}>
<div className={styles.dataSourceTitleContainer}>
<Icon data={props.icon} className={styles.dataSourceIcon}/>
{props.url && urlTitle ?
// Here we want to place ellipsis in the middle of the long url, e.g. example.com/lo...ng/1234.sqlite
<a href={props.url} className={styles.title} title={props.title}>
<span className={styles.ellipsis} style={{'--max-width': '130px'} as React.CSSProperties}>{urlTitle.slice(0, -24)}</span>
{urlTitle.slice(-24)}
</a> :
<span className={styles.ellipsis}>{props.title}</span>}
</div>
<div className={styles.dataSourceStatus}><span>{props.success ? 'OK' : (props.statusCode && typeof props.statusCode === 'number' ? `E${props.statusCode}` : 'ERR')}</span>
<div
className={classNames(styles.dataSourceStatusCircle, props.success ? styles.dataSourceStatusCircleOnline : styles.dataSourceStatusCircleOffline)}></div>
</div>
</div>;
}
55 changes: 55 additions & 0 deletions lib/static/new-ui/components/InfoPanel/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.divider {
margin: 20px 0;
}

.info-panel a:link, .info-panel a:visited {
color: var(--color-link);
}

.info-panel a:hover {
color: var(--color-link-hover);
}

.extra-items-list {
padding-left: 24px;
list-style: none;
}

.extra-items-list li {
position: relative;
margin-bottom: 12px;
}

.extra-items-list li::before {
content: '';
height: 4px;
width: 4px;
border-radius: 100vh;
background-color: #000;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -10px;
}

.extra-items-list a {
display: inline-block;
}

.extra-items-list a::first-letter {
text-transform: uppercase;
}

.data-source-heading {
display: flex;
justify-content: space-between;
font-weight: 450;
border-bottom: 1px solid var(--g-divider-color);
padding-bottom: 8px;
}

.data-source-list {
display: flex;
flex-direction: column;
}

Loading

0 comments on commit 9dca0e8

Please sign in to comment.