Skip to content

feat: add plugins to new ui #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion lib/gui/routes/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
logError
} from '../../server-utils';
import {ReporterConfig} from '../../types';
import {getPluginMiddlewareRoute} from '../../static/modules/utils/pluginMiddlewareRoute';

export const initPluginsRoutes = (router: Router, pluginConfig: ReporterConfig): Router => {
if (!pluginConfig.pluginsEnabled) {
Expand Down Expand Up @@ -40,7 +41,7 @@ export const initPluginsRoutes = (router: Router, pluginConfig: ReporterConfig):
const pluginRouter = Router();
initPluginMiddleware(pluginRouter);

router.use(`/plugin-routes/${pluginName}`, pluginRouter);
router.use(getPluginMiddlewareRoute(pluginName), pluginRouter);
} catch (err: unknown) {
logError(err as Error);
}
Expand Down
16 changes: 15 additions & 1 deletion lib/static/modules/load-plugin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import * as JSXRuntime from 'react/jsx-runtime';
import * as Redux from 'redux';
import * as ReactRedux from 'react-redux';
import _ from 'lodash';
Expand All @@ -10,12 +12,16 @@ import reduceReducers from 'reduce-reducers';
import immer from 'immer';
import * as reselect from 'reselect';
import axios from 'axios';
import * as GravityUI from '@gravity-ui/uikit';
import * as selectors from './selectors';
import actionNames from './action-names';
import Details from '../components/details';
import {getPluginMiddlewareRoute} from '@/static/modules/utils/pluginMiddlewareRoute';

const whitelistedDeps = {
'react': React,
'react/jsx-runtime': JSXRuntime,
'react-dom': ReactDOM,
'redux': Redux,
'react-redux': ReactRedux,
'lodash': _,
Expand All @@ -27,6 +33,7 @@ const whitelistedDeps = {
'immer': immer,
'reselect': reselect,
'axios': axios,
'@gravity-ui/uikit': GravityUI,
'components': {
Details
}
Expand Down Expand Up @@ -92,7 +99,14 @@ async function initPlugin(plugin, pluginName, pluginConfig) {
const depArgs = deps.map(dep => whitelistedDeps[dep]);
// cyclic dep, resolve it dynamically
const actions = await import('./actions');
return plugin(...depArgs, {pluginName, pluginConfig, actions, actionNames, selectors});
return plugin(...depArgs, {
pluginName,
pluginConfig,
actions,
actionNames,
selectors,
middlewareRoutePrefix: getPluginMiddlewareRoute(pluginName)
});
}

return plugin;
Expand Down
7 changes: 7 additions & 0 deletions lib/static/modules/utils/pluginMiddlewareRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const encodePackageNameRoute = (packageName: string): string => {
return packageName.replace(/[/@.]/g, '__');
};

export const getPluginMiddlewareRoute = (pluginName: string): string => {
return `/plugin-routes/${encodePackageNameRoute(pluginName)}`;
};
12 changes: 12 additions & 0 deletions lib/static/new-ui/components/AsidePanel/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
--g-text-body-short-font-size: 15px;
}

.titleRow {
display: flex;
align-items: center;
gap: 8px;
}

.divider {
margin: 12px 0 20px 0;
}

.description {
color: var(--g-color-private-black-400);
margin: 8px 0 16px 0;
line-height: 1.3;
}
8 changes: 7 additions & 1 deletion lib/static/new-ui/components/AsidePanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import styles from './index.module.css';

interface AsidePanelProps {
title: string;
description?: string;
icon?: ReactNode;
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>
<div className={styles.titleRow}>
{props.icon}
<h2 className={classNames('text-display-1')}>{props.title}</h2>
</div>
{props.description && <div className={styles.description}>{props.description}</div>}
<Divider className={styles.divider} orientation={'horizontal'} />
{props.children}
</div>;
Expand Down
10 changes: 5 additions & 5 deletions lib/static/new-ui/components/MainLayout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {UiModeHintNotification} from '@/static/new-ui/components/UiModeHintNotif
import styles from '@/static/new-ui/components/MainLayout/index.module.css';
import {getIsInitialized} from '@/static/new-ui/store/selectors';
import useLocalStorage from '@/static/hooks/useLocalStorage';
import {PanelId} from '@/static/new-ui/components/MainLayout/index';
import {PanelId, PanelIdStatic} from '@/static/new-ui/components/MainLayout/index';

interface FooterProps {
visiblePanel: PanelId | null;
Expand Down Expand Up @@ -45,13 +45,13 @@ export function Footer(props: FooterProps): ReactNode {
}
}, [props.visiblePanel]);

const isInfoCurrent = props.visiblePanel === PanelId.Info;
const isSettingsCurrent = props.visiblePanel === PanelId.Settings;
const isInfoCurrent = props.visiblePanel === PanelIdStatic.Info;
const isSettingsCurrent = props.visiblePanel === PanelIdStatic.Settings;

return <>
<UiModeHintNotification isVisible={isHintVisible} onClose={(): void => setIsHintVisible(false)} />
<FooterItem compact={false} item={{
id: PanelId.Info,
id: PanelIdStatic.Info,
title: 'Info',
onItemClick: props.onFooterItemClick,
current: isInfoCurrent,
Expand All @@ -66,7 +66,7 @@ export function Footer(props: FooterProps): ReactNode {
})
}} />
<FooterItem compact={false} item={{
id: PanelId.Settings,
id: PanelIdStatic.Settings,
title: 'Settings',
onItemClick: props.onFooterItemClick,
current: isSettingsCurrent,
Expand Down
120 changes: 77 additions & 43 deletions lib/static/new-ui/components/MainLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {AsideHeader, MenuItem as GravityMenuItem} from '@gravity-ui/navigation';
import classNames from 'classnames';
import React, {ReactNode, useState} from 'react';
import React, {ReactNode, useMemo, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useNavigate, matchPath, useLocation} from 'react-router-dom';
import {matchPath, useLocation, useNavigate} from 'react-router-dom';

import {getIsInitialized} from '@/static/new-ui/store/selectors';
import {SettingsPanel} from '@/static/new-ui/components/SettingsPanel';
Expand All @@ -12,15 +12,22 @@ import {Footer} from './Footer';
import {EmptyReportCard} from '@/static/new-ui/components/Card/EmptyReportCard';
import {InfoPanel} from '@/static/new-ui/components/InfoPanel';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';
import {setSectionSizes} from '../../../modules/actions/suites-page';
import {ArrowLeftToLine, ArrowRightFromLine} from '@gravity-ui/icons';
import {setSectionSizes} from '@/static/modules/actions';
import {ArrowLeftToLine, ArrowRightFromLine, PlugConnection} from '@gravity-ui/icons';
import {isSectionHidden} from '../../features/suites/utils';
import {PluginPanel} from '@/static/new-ui/features/plugins/components/PluginPanel';
import {useExtensionPoint} from '@/static/new-ui/features/plugins/hooks/useExtensionPoint';
import {ExtensionPointName} from '@/static/new-ui/features/plugins/types';

export enum PanelId {
export enum PanelIdStatic {
Settings = 'settings',
Info = 'info',
}

export type PluginPanelId = `plugin-${string}`;

export type PanelId = PanelIdStatic | PluginPanelId;

interface MenutItemPage {
title: string;
url: string;
Expand All @@ -38,50 +45,77 @@ export function MainLayout(props: MainLayoutProps): ReactNode {
const location = useLocation();
const analytics = useAnalytics();

const menuItems: GravityMenuItem[] = props.pages.map(item => ({
id: item.url,
title: item.title,
icon: item.icon,
current: Boolean(matchPath(`${item.url.replace(/\/$/, '')}/*`, location.pathname)),
onItemClick: (): void => {
analytics?.trackFeatureUsage({featureName: `Go to ${item.url} page`});
navigate(item.url);
const plugins = useExtensionPoint(ExtensionPointName.Menu);

const [visiblePanel, setVisiblePanel] = useState<PanelId | null>(null);

const onFooterItemClick = (item: Pick<GravityMenuItem, 'id'>): void => {
if (visiblePanel === item.id) {
setVisiblePanel(null);
} else {
setVisiblePanel(item.id as PanelId);
analytics?.trackFeatureUsage({featureName: `Open ${item.id} panel`});
}
}));
};

const currentSuitesPageSectionSizes = useSelector(state => state.ui.suitesPage.sectionSizes);
const backupSuitesPageSectionSizes = useSelector(state => state.ui.suitesPage.backupSectionSizes);
if (/\/suites/.test(location.pathname)) {
const shouldExpandTree = isSectionHidden(currentSuitesPageSectionSizes[0]);
menuItems.push(
{id: 'divider', type: 'divider', title: '-'},
{
id: 'expand-collapse-tree',
title: shouldExpandTree ? 'Expand tree' : 'Collapse tree',
icon: shouldExpandTree ? ArrowRightFromLine : ArrowLeftToLine,
onItemClick: (): void => {
dispatch(setSectionSizes({sizes: shouldExpandTree ? backupSuitesPageSectionSizes : [0, 100]}));
},
qa: 'expand-collapse-suites-tree'

const menuItems: GravityMenuItem[] = useMemo(() => {
const mainPages = props.pages.map(item => ({
id: item.url,
title: item.title,
icon: item.icon,
current: Boolean(matchPath(`${item.url.replace(/\/$/, '')}/*`, location.pathname)),
onItemClick: (): void => {
analytics?.trackFeatureUsage({featureName: `Go to ${item.url} page`});
navigate(item.url);
}
);
}
}));

const pluginPages = plugins.map(plugin => ({
id: `plugin-${plugin.name}`,
title: plugin.PluginComponent.metadata.name ?? plugin.name,
icon: PlugConnection,
current: visiblePanel === `plugin-${plugin.name}`,
onItemClick: () => onFooterItemClick({
id: `plugin-${plugin.name}`})
}));

const extraItems: GravityMenuItem[] = [];

if (/\/suites/.test(location.pathname)) {
const shouldExpandTree = isSectionHidden(currentSuitesPageSectionSizes[0]);
extraItems.push(
{id: 'divider', type: 'divider', title: '-'},
{
id: 'expand-collapse-tree',
title: shouldExpandTree ? 'Expand tree' : 'Collapse tree',
icon: shouldExpandTree ? ArrowRightFromLine : ArrowLeftToLine,
onItemClick: (): void => {
dispatch(setSectionSizes({sizes: shouldExpandTree ? backupSuitesPageSectionSizes : [0, 100]}));
},
qa: 'expand-collapse-suites-tree'
}
);
}

return [...mainPages, pluginPages.length > 0 && {id: 'divider', type: 'divider', title: '-'}, ...pluginPages, ...extraItems].filter(Boolean) as GravityMenuItem[];
}, [props.pages, plugins, location.pathname, visiblePanel, currentSuitesPageSectionSizes, backupSuitesPageSectionSizes]);

const pluginsPanels = useMemo(() => {
return plugins.map(plugin => ({
id: `plugin-${plugin.name}`,
content: <PluginPanel plugin={plugin}/>,
visible: visiblePanel === `plugin-${plugin.name}`
}));
}, [plugins, visiblePanel]);

const isInitialized = useSelector(getIsInitialized);

const browsersById = useSelector(state => state.tree.browsers.byId);
const isReportEmpty = isInitialized && Object.keys(browsersById).length === 0;

const [visiblePanel, setVisiblePanel] = useState<PanelId | null>(null);
const onFooterItemClick = (item: GravityMenuItem): void => {
if (visiblePanel === item.id) {
setVisiblePanel(null);
} else {
setVisiblePanel(item.id as PanelId);
analytics?.trackFeatureUsage({featureName: `Open ${item.id} panel`});
}
};

return <AsideHeader
className={classNames({'aside-header--initialized': isInitialized})}
logo={{text: 'Testplane UI', iconSrc: TestplaneIcon, iconSize: 32, onClick: () => navigate('/suites')}}
Expand All @@ -100,14 +134,14 @@ export function MainLayout(props: MainLayoutProps): ReactNode {
hideCollapseButton={true}
renderFooter={(): ReactNode => <Footer visiblePanel={visiblePanel} onFooterItemClick={onFooterItemClick}/>}
panelItems={[{
id: PanelId.Info,
id: PanelIdStatic.Info,
children: <InfoPanel />,
visible: visiblePanel === PanelId.Info
visible: visiblePanel === PanelIdStatic.Info
}, {
id: PanelId.Settings,
id: PanelIdStatic.Settings,
children: <SettingsPanel />,
visible: visiblePanel === PanelId.Settings
}]}
visible: visiblePanel === PanelIdStatic.Settings
}, ...pluginsPanels]}
onClosePanel={(): void => setVisiblePanel(null)}
/>;
}
6 changes: 6 additions & 0 deletions lib/static/new-ui/components/PanelSection/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
margin: 8px 0 16px 0;
line-height: 1.3;
}

.titleRow {
display: flex;
align-items: center;
gap: 6px;
}
6 changes: 5 additions & 1 deletion lib/static/new-ui/components/PanelSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import styles from './index.module.css';
interface PanelSectionProps {
title: string;
description?: ReactNode;
icon?: ReactNode;
children?: ReactNode;
}

export function PanelSection(props: PanelSectionProps): ReactNode {
return <div>
<div className={classNames('text-header-1')}>{props.title}</div>
<div className={styles.titleRow}>
{props.icon}
<div className={classNames('text-header-1')}>{props.title}</div>
</div>
{props.description && <div className={styles.description}>{props.description}</div>}
{props.children}
</div>;
Expand Down
4 changes: 4 additions & 0 deletions lib/static/new-ui/components/SettingsPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {PanelSection} from '@/static/new-ui/components/PanelSection';
import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics';
import {NamedSwitch} from '@/static/new-ui/components/NamedSwitch';
import {isApiErrorResponse, updateTimeTravelSettings} from '../../utils/api';
import {ExtensionPoint} from '@/static/new-ui/features/plugins/components/ExtensionPoint';
import {ExtensionPointName} from '@/static/new-ui/features/plugins/types';

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

Expand Down Expand Up @@ -79,5 +81,7 @@ export function SettingsPanel(): ReactNode {
onUpdate={onTimeTravelRecommendedSettingsToggle}
/>
</PanelSection>}

<ExtensionPoint name={ExtensionPointName.Settings} />
</AsidePanel>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.host {
width: 100%;
height: max-content;
display: flex;
flex-direction: column;
}

.divider {
margin: 20px 0;
}
Loading