Skip to content

Commit

Permalink
feat: Plugin Overlay for static, high priority, immutable configurati…
Browse files Browse the repository at this point in the history
…on (#1018)

* refactor: create store only once

* feat: plugin overlay

* chore: remove unused stubs

* refactor: use refetchOnMountOrArgChange

* chore: remove unused code
  • Loading branch information
devcatalin authored Feb 26, 2024
1 parent ca255a4 commit 73bbec2
Show file tree
Hide file tree
Showing 28 changed files with 172 additions and 126 deletions.
1 change: 1 addition & 0 deletions packages/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './src/utils';
export * from './src/StoreProvider';
export * from './src/PluginResolver';
export * from './src/usePluginSystem';
export * from './src/usePluginOverlay';
export type {Plugin, PluginEntry} from './src/internal/Plugin';
3 changes: 3 additions & 0 deletions packages/plugins/src/internal/Plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {PluginOverlay} from './PluginOverlay';
import type {PluginScope} from './PluginScope';
import {PluginDetails as PluginDetailsSymbol, PluginInit} from './symbols';
import type {PluginConfig, PluginConfigInput, PluginDetails, PluginScopeStateFor, PluginState} from './types';
Expand All @@ -7,6 +8,8 @@ export class Plugin<T extends PluginState> {
public readonly [PluginInit]: (context: PluginScope<PluginScopeStateFor<T>>, config: T['config']) => void;
public readonly [PluginDetailsSymbol]: PluginDetails<T>;

public readonly overlay = new PluginOverlay();

public constructor(
config: PluginDetails<T>,
initFn: (tk: PluginScope<PluginScopeStateFor<T>>, config: PluginConfig<T['config']>) => void
Expand Down
32 changes: 32 additions & 0 deletions packages/plugins/src/internal/PluginOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {PluginOverlayContext} from './symbols';
import {PluginProvider} from './types';

export class PluginOverlay {
private [PluginOverlayContext]: Record<string, any> = {};
private providerCreator: (() => PluginProvider<any>) | null = null;

public getContext() {
return this[PluginOverlayContext];
}

public setContext(context: Record<string, any> | ((oldContext: Record<string, any>) => Record<string, any>)) {
if (typeof context === 'function') {
this[PluginOverlayContext] = context(this[PluginOverlayContext]);
return;
}
this[PluginOverlayContext] = context;
}

public appendContext(context: Record<string, any>) {
this[PluginOverlayContext] = {...this[PluginOverlayContext], ...context};
}

public useProvider() {
if (!this.providerCreator) return null;
return this.providerCreator();
}

public provider(pc: typeof this.providerCreator) {
this.providerCreator = pc;
}
}
1 change: 1 addition & 0 deletions packages/plugins/src/internal/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export const PluginScopeScheduleUpdate = Symbol.for('schedule informing about ch
export const PluginScopeEmitChange = Symbol.for('inform about data change');
export const PluginScopeSubscribeChange = Symbol.for('subscribe to data change');
export const PluginScopeListeners = Symbol.for('subscription listeners list');
export const PluginOverlayContext = Symbol.for('context used only for overlay');
29 changes: 29 additions & 0 deletions packages/plugins/src/usePluginOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {FC, PropsWithChildren, ReactElement, createElement, useCallback} from 'react';

import {Plugin, PluginEntry} from './internal/Plugin';
import {PluginOverlay} from './internal/PluginOverlay';

export const usePluginOverlay = (plugins: PluginEntry<any>[]) => {
const createProviderTree = useCallback((overlays: PluginOverlay[], children: ReactElement): ReactElement => {
if (overlays.length === 0) {
return children;
}

const lastOverlay = overlays[overlays.length - 1];
const provider = lastOverlay.useProvider();
const newChildren = provider ? createElement(provider.type, provider.props, children) : children;

return createProviderTree(overlays.slice(0, -1), newChildren);
}, []);

const ProviderComponent = useCallback<FC<PropsWithChildren<any>>>(
({children}) =>
createProviderTree(
plugins.filter((plugin): plugin is Plugin<any> => 'overlay' in plugin).map(plugin => plugin.overlay),
children
),
[createProviderTree]
);

return ProviderComponent;
};
32 changes: 17 additions & 15 deletions packages/web/src/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useMemo} from 'react';
import {Layout} from 'antd';
import {Content} from 'antd/lib/layout/layout';

import {usePluginSystem} from '@testkube/plugins';
import {usePluginOverlay, usePluginSystem} from '@testkube/plugins';

import {ReactComponent as Logo} from '@assets/testkube-symbol-color.svg';

Expand All @@ -26,7 +26,6 @@ import ModalPlugin from '@plugins/modal/plugin';
import PermissionsPlugin from '@plugins/permissions/plugin';
import PromoBannersPlugin from '@plugins/promo-banners/plugin';
import RouterPlugin from '@plugins/router/plugin';
import RtkResetOnApiChangePlugin from '@plugins/rtk-reset-on-api-change/plugin';
import RtkPlugin from '@plugins/rtk/plugin';
import SettingsPlugin from '@plugins/settings/plugin';
import SiderCloudMigratePlugin from '@plugins/sider-cloud-migrate/plugin';
Expand All @@ -51,7 +50,6 @@ const AppRoot: React.FC = () => {
ConfigPlugin.configure({slackUrl: externalLinks.slack}),
RouterPlugin.configure({baseUrl: env.basename || ''}),
PermissionsPlugin.configure({resolver: new BasePermissionsResolver()}),
RtkResetOnApiChangePlugin,
RtkPlugin,
ModalPlugin,
SiderLogoPlugin.configure({logo: <Logo />}),
Expand All @@ -67,6 +65,8 @@ const AppRoot: React.FC = () => {
);
const [PluginProvider, {routes}] = usePluginSystem(plugins);

const PluginOverlayProvider = usePluginOverlay(plugins);

return (
<ErrorBoundary>
<TelemetryProvider
Expand All @@ -76,18 +76,20 @@ const AppRoot: React.FC = () => {
debug={env.debugTelemetry}
paused
>
<PluginProvider>
<Layout>
<Sider />
<StyledLayoutContentWrapper>
<Content>
<ErrorBoundary>
<App routes={routes} />
</ErrorBoundary>
</Content>
</StyledLayoutContentWrapper>
</Layout>
</PluginProvider>
<PluginOverlayProvider>
<PluginProvider>
<Layout>
<Sider />
<StyledLayoutContentWrapper>
<Content>
<ErrorBoundary>
<App routes={routes} />
</ErrorBoundary>
</Content>
</StyledLayoutContentWrapper>
</Layout>
</PluginProvider>
</PluginOverlayProvider>
</TelemetryProvider>
</ErrorBoundary>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const LabelsSelect: React.FC<LabelsSelectProps> = props => {
const {data, isFetching} = useGetLabelsQuery(null, {
pollingInterval: PollingIntervals.default,
skip: Boolean(options) || !isClusterAvailable,
refetchOnMountOrArgChange: true,
});

const formattedValue = useMemo(() => (value || []).map(label => ({label, value: label})), [value]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ const TestActionsDropdown: React.FC<ActionsDropdownProps> = props => {

const {data: metrics, refetch} = useGetTestExecutionMetricsQuery(
{id: name, last: 7, limit: 13},
{skip: !isInViewport || !isSystemAvailable, pollingInterval: PollingIntervals.halfMin}
{
skip: !isInViewport || !isSystemAvailable,
pollingInterval: PollingIntervals.halfMin,
refetchOnMountOrArgChange: true,
}
);

const executions: ExecutionMetrics[] = useMemo(() => metrics?.executions ?? [], [metrics]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ const TestSuiteActionsDropdown: React.FC<ActionsDropdownProps> = props => {

const {data: metrics, refetch} = useGetTestSuiteExecutionMetricsQuery(
{id: name, last: 7, limit: 13},
{skip: !isInViewport || !isSystemAvailable, pollingInterval: PollingIntervals.halfMin}
{
skip: !isInViewport || !isSystemAvailable,
pollingInterval: PollingIntervals.halfMin,
refetchOnMountOrArgChange: true,
}
);

const executions: ExecutionMetrics[] = useMemo(() => metrics?.executions ?? [], [metrics]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,18 @@ const EntityDetailsLayer: FC<PropsWithChildren<EntityDetailsLayerProps>> = ({
{
pollingInterval: PollingIntervals.long,
skip: !isSystemAvailable || daysFilterValue === undefined,
refetchOnMountOrArgChange: true,
}
);

const {data: rawMetrics, refetch: refetchMetrics} = useGetMetrics(
{id, last: daysFilterValue},
{skip: !isSystemAvailable}
{skip: !isSystemAvailable, refetchOnMountOrArgChange: true}
);
const {data: rawDetails, error} = useGetEntityDetails(id, {
pollingInterval: PollingIntervals.long,
skip: !isSystemAvailable,
refetchOnMountOrArgChange: true,
});

const isV2 = isTestSuiteV2(rawDetails);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const ExecutionDetailsLayer: FC<PropsWithChildren<ExecutionDetailsLayerProps>> =
const {data: rawFetchedData, error} = useGetExecutionDetails(execId!, {
pollingInterval: PollingIntervals.everySecond,
skip: !isClusterAvailable || !execId || (data?.id === execId && isExecutionFinished(data)),
refetchOnMountOrArgChange: true,
});
const fetchedData = rawFetchedData?.id === execId ? rawFetchedData : null;
const isV2 = isTestSuiteV2Execution(fetchedData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const LabelsFilter: React.FC<EntityFilters> = props => {
const {data} = useGetLabelsQuery(null, {
pollingInterval: PollingIntervals.default,
skip: !isClusterAvailable,
refetchOnMountOrArgChange: true,
});

const [isVisible, setIsVisible] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ const TestSuiteCard: FC<TestSuiteCardProps> = ({item: {testSuite, latestExecutio

const {data: metrics} = useGetTestSuiteExecutionMetricsQuery(
{id: testSuite.name, last: 7, limit: 13},
{skip: !isInViewport || !isSystemAvailable, pollingInterval: PollingIntervals.halfMin}
{
skip: !isInViewport || !isSystemAvailable,
pollingInterval: PollingIntervals.halfMin,
refetchOnMountOrArgChange: true,
}
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const TestSuitesList: React.FC = () => {
} = useGetTestSuitesQuery(filters || null, {
pollingInterval: PollingIntervals.everySecond,
skip: !isSystemAvailable,
refetchOnMountOrArgChange: true,
});
useTestSuitesSync({testSuites});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const TestExecutionArtifacts: React.FC<TestExecutionArtifactsProps> = props => {
const {data, isLoading, error} = useGetTestExecutionArtifactsQuery(id, {
skip: !isSystemAvailable,
pollingInterval: PollingIntervals.everyTwoSeconds,
refetchOnMountOrArgChange: true,
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ const TestCard: FC<TestCardProps> = ({item: {test, latestExecution}, onClick}) =

const {data: metrics} = useGetTestExecutionMetricsQuery(
{id: test.name, last: 7, limit: 13},
{skip: !isInViewport || !isSystemAvailable, pollingInterval: PollingIntervals.halfMin}
{
skip: !isInViewport || !isSystemAvailable,
pollingInterval: PollingIntervals.halfMin,
refetchOnMountOrArgChange: true,
}
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const TestsList: React.FC = () => {
} = useGetTestsQuery(filters, {
pollingInterval: PollingIntervals.everySecond,
skip: !isSystemAvailable,
refetchOnMountOrArgChange: true,
});
useTestsSync({tests});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ const WebhooksList: FC = () => {
data: webhooks,
error,
isLoading,
} = useGetWebhooksQuery(null, {pollingInterval: PollingIntervals.everyTwoSeconds, skip: !isClusterAvailable});
} = useGetWebhooksQuery(null, {
pollingInterval: PollingIntervals.everyTwoSeconds,
skip: !isClusterAvailable,
refetchOnMountOrArgChange: true,
});

const mayCreate = usePermission(Permissions.createEntity);

Expand Down
10 changes: 4 additions & 6 deletions packages/web/src/plugins/cluster-status/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ import {createPlugin, data, external} from '@testkube/plugins';
import {SystemAccess, useSystemAccess} from '@hooks/useSystemAccess';

import type GeneralPlugin from '@plugins/general/plugin';
import type RtkPlugin from '@plugins/rtk/plugin';
import RtkPlugin from '@plugins/rtk/plugin';

import {configApi, useGetClusterConfigQuery} from '@services/config';

import {safeRefetch} from '@utils/fetchUtils';

const generalStub = external<typeof GeneralPlugin>();
const rtkStub = external<typeof RtkPlugin>();

export default createPlugin('dashboard/cluster-status')
.needs(generalStub.data('useApiEndpoint'))
.needs(rtkStub.slots('rtkServices'))

.define(data<boolean>()('isClusterAvailable', 'isSystemAvailable', 'isTelemetryEnabled'))

Expand All @@ -38,6 +36,6 @@ export default createPlugin('dashboard/cluster-status')
}, [apiEndpoint]);
})

.init(tk => {
tk.slots.rtkServices.add(configApi);
});
.init();

RtkPlugin.overlay.appendContext({configApi});
7 changes: 3 additions & 4 deletions packages/web/src/plugins/executors/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ExecutorsList from '@pages/Executors/ExecutorsList/ExecutorsList';

import type ClusterStatusPlugin from '@plugins/cluster-status/plugin';
import type GeneralPlugin from '@plugins/general/plugin';
import type RtkPlugin from '@plugins/rtk/plugin';
import RtkPlugin from '@plugins/rtk/plugin';

import {executorsApi, useGetExecutorsQuery} from '@services/executors';

Expand All @@ -26,13 +26,11 @@ import {PollingIntervals} from '@utils/numbers';

const generalStub = external<typeof GeneralPlugin>();
const clusterStatusStub = external<typeof ClusterStatusPlugin>();
const rtkStub = external<typeof RtkPlugin>();

export default createPlugin('dashboard/executors')
.needs(clusterStatusStub.data('useSystemAccess', 'SystemAccess'))
.needs(generalStub.slots('siderItems'))
.needs(generalStub.data('useApiEndpoint'))
.needs(rtkStub.slots('rtkServices'))

.route('/executors', <ExecutorsList />)
.route('/executors/:id', <ExecutorDetails />)
Expand Down Expand Up @@ -60,6 +58,7 @@ export default createPlugin('dashboard/executors')
})

.init(tk => {
tk.slots.rtkServices.add(executorsApi);
tk.slots.siderItems.add({path: '/executors', icon: ExecutorsIcon, title: 'Executors'}, {order: -80});
});

RtkPlugin.overlay.appendContext({executorsApi});
13 changes: 4 additions & 9 deletions packages/web/src/plugins/labels/plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import {createPlugin, external} from '@testkube/plugins';
import {createPlugin} from '@testkube/plugins';

import type RtkPlugin from '@plugins/rtk/plugin';
import RtkPlugin from '@plugins/rtk/plugin';

import {labelsApi} from '@services/labels';

const rtkStub = external<typeof RtkPlugin>();
export default createPlugin('oss/labels').init();

export default createPlugin('oss/labels')
.needs(rtkStub.slots('rtkServices'))

.init(tk => {
tk.slots.rtkServices.add(labelsApi);
});
RtkPlugin.overlay.appendContext({labelsApi});
7 changes: 0 additions & 7 deletions packages/web/src/plugins/rtk-reset-on-api-change/hooks.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/web/src/plugins/rtk-reset-on-api-change/plugin.tsx

This file was deleted.

Loading

0 comments on commit 73bbec2

Please sign in to comment.