Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 src/config/dynamic/dynamic.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import extendedDomainInfoEnabled from './resolvers/extended-domain-info-enabled'
import { type ExtendedDomainInfoEnabledConfig } from './resolvers/extended-domain-info-enabled.types';
import failoverHistoryEnabled from './resolvers/failover-history-enabled';
import historyPageV2Enabled from './resolvers/history-page-v2-enabled';
import { type HistoryPageV2EnabledConfigValue } from './resolvers/history-page-v2-enabled.types';
import workflowActionsEnabled from './resolvers/workflow-actions-enabled';
import {
type WorkflowActionsEnabledResolverParams,
Expand Down Expand Up @@ -76,7 +77,7 @@ const dynamicConfigs: {
>;
HISTORY_PAGE_V2_ENABLED: ConfigAsyncResolverDefinition<
undefined,
boolean,
HistoryPageV2EnabledConfigValue,
'request',
true
>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG = [
'DISABLED',
'OPT_IN',
'OPT_OUT',
'ENABLED',
] as const;

export default HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG;
24 changes: 19 additions & 5 deletions src/config/dynamic/resolvers/history-page-v2-enabled.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG from './history-page-v2-enabled-values.config';
import { type HistoryPageV2EnabledConfigValue } from './history-page-v2-enabled.types';

/**
* WIP: Returns whether the new Workflow History (V2) page is enabled
* WIP: Returns the configuration value for the new Workflow History (V2) page
*
* To configure the new Workflow History (V2) page, set the CADENCE_HISTORY_PAGE_V2_ENABLED env variable to one of the following:
* - `DISABLED` - disable the feature entirely (default if env var is not set or invalid)
* - `OPT_IN` - allow users to view the new page using a button on the old page
* - `OPT_OUT` - default to the new page with an option to fall back to the old page
* - `ENABLED` - completely enable the new page with no option to fall back
*
* To enable the new Workflow History (V2) page, set the CADENCE_HISTORY_PAGE_V2_ENABLED env variable to true.
* For further customization, override the implementation of this resolver.
*
* @returns {Promise<boolean>} Whether Workflow History (V2) page is enabled.
* @returns {Promise<HistoryPageV2EnabledConfigValue>} The configuration value for Workflow History (V2) page.
*/
export default async function historyPageV2Enabled(): Promise<boolean> {
return process.env.CADENCE_HISTORY_PAGE_V2_ENABLED === 'true';
export default async function historyPageV2Enabled(): Promise<HistoryPageV2EnabledConfigValue> {
const envValue = process.env.CADENCE_HISTORY_PAGE_V2_ENABLED;

if (HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG.includes(envValue as any)) {
return envValue as HistoryPageV2EnabledConfigValue;
}

return 'DISABLED';
}
4 changes: 4 additions & 0 deletions src/config/dynamic/resolvers/history-page-v2-enabled.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG from './history-page-v2-enabled-values.config';

export type HistoryPageV2EnabledConfigValue =
(typeof HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG)[number];
3 changes: 2 additions & 1 deletion src/config/dynamic/resolvers/schemas/resolver-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from 'zod';

import { type ResolverSchemas } from '../../../../utils/config/config.types';
import HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG from '../history-page-v2-enabled-values.config';
import WORKFLOW_ACTIONS_DISABLED_VALUES_CONFIG from '../workflow-actions-disabled-values.config';

const workflowActionsEnabledValueSchema = z.enum([
Expand Down Expand Up @@ -72,7 +73,7 @@ const resolverSchemas: ResolverSchemas = {
},
HISTORY_PAGE_V2_ENABLED: {
args: z.undefined(),
returnType: z.boolean(),
returnType: z.enum(HISTORY_PAGE_V2_ENABLED_VALUES_CONFIG),
},
};

Expand Down
2 changes: 1 addition & 1 deletion src/utils/config/__fixtures__/resolved-config-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ const mockResolvedConfigValues: LoadedConfigResolvedValues = {
WORKFLOW_DIAGNOSTICS_ENABLED: false,
ARCHIVAL_DEFAULT_SEARCH_ENABLED: false,
FAILOVER_HISTORY_ENABLED: false,
HISTORY_PAGE_V2_ENABLED: false,
HISTORY_PAGE_V2_ENABLED: 'DISABLED',
};
export default mockResolvedConfigValues;
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@ async function setup({
value={{
ungroupedViewUserPreference: ungroupedViewPreference ?? null,
setUngroupedViewUserPreference: mockSetUngroupedViewUserPreference,
isWorkflowHistoryV2Enabled: false,
setIsWorkflowHistoryV2Enabled: jest.fn(),
}}
>
<WorkflowHistoryV2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { Suspense } from 'react';

import { renderHook, waitFor, act } from '@/test-utils/rtl';

import { type UseSuspenseConfigValueResult } from '@/hooks/use-config-value/use-config-value.types';
import useSuspenseConfigValue from '@/hooks/use-config-value/use-suspense-config-value';
import * as localStorageModule from '@/utils/local-storage';
import workflowHistoryUserPreferencesConfig from '@/views/workflow-history/config/workflow-history-user-preferences.config';

import useIsWorkflowHistoryV2Enabled from '../use-is-workflow-history-v2-enabled';

jest.mock('@/hooks/use-config-value/use-suspense-config-value');
jest.mock('@/utils/local-storage', () => ({
getLocalStorageValue: jest.fn(),
setLocalStorageValue: jest.fn(),
clearLocalStorageValue: jest.fn(),
}));

const mockUseSuspenseConfigValue =
useSuspenseConfigValue as jest.MockedFunction<any>;

describe(useIsWorkflowHistoryV2Enabled.name, () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('should return true when config value is ENABLED', async () => {
const { result } = setup({ configValue: 'ENABLED' });

await waitFor(() => {
expect(result.current[0]).toBe(true);
});
});

it('should return true when config value is OPT_OUT', async () => {
const { result } = setup({ configValue: 'OPT_OUT' });

await waitFor(() => {
expect(result.current[0]).toBe(true);
});
});

it('should return false when config value is DISABLED', async () => {
const { result } = setup({ configValue: 'DISABLED' });

await waitFor(() => {
expect(result.current[0]).toBe(false);
});
});

it('should return false when config value is OPT_IN and localStorage has no value', async () => {
const { result } = setup({ configValue: 'OPT_IN' });

await waitFor(() => {
expect(result.current[0]).toBe(false);
});
});

it('should return true when config value is OPT_IN and localStorage has true', async () => {
const { result } = setup({
configValue: 'OPT_IN',
localStorageValue: true,
});

await waitFor(() => {
expect(result.current[0]).toBe(true);
});
});

it('should return false when config value is OPT_IN and localStorage has false', async () => {
const { result } = setup({
configValue: 'OPT_IN',
localStorageValue: false,
});

await waitFor(() => {
expect(result.current[0]).toBe(false);
});
});

it('should not allow setting value when config is DISABLED', async () => {
const { result, mockSetLocalStorageValue } = setup({
configValue: 'DISABLED',
});

await waitFor(() => {
expect(result.current[0]).toBe(false);
});

act(() => {
result.current[1](true);
});

expect(result.current[0]).toBe(false);
expect(mockSetLocalStorageValue).not.toHaveBeenCalled();
});

it('should not allow setting value when config is ENABLED', async () => {
const { result, mockSetLocalStorageValue } = setup({
configValue: 'ENABLED',
});

await waitFor(() => {
expect(result.current[0]).toBe(true);
});

act(() => {
result.current[1](false);
});

expect(result.current[0]).toBe(true);
expect(mockSetLocalStorageValue).not.toHaveBeenCalled();
});

it('should allow setting value when config is OPT_OUT', async () => {
const { result, mockSetLocalStorageValue } = setup({
configValue: 'OPT_OUT',
});

await waitFor(() => {
expect(result.current[0]).toBe(true);
});

act(() => {
result.current[1](false);
});

expect(result.current[0]).toBe(false);
expect(mockSetLocalStorageValue).not.toHaveBeenCalled();
});

it('should allow setting value and update localStorage when config is OPT_IN', async () => {
const { result, mockSetLocalStorageValue } = setup({
configValue: 'OPT_IN',
localStorageValue: false,
});

await waitFor(() => {
expect(result.current[0]).toBe(false);
});

act(() => {
result.current[1](true);
});

expect(result.current[0]).toBe(true);
expect(mockSetLocalStorageValue).toHaveBeenCalledWith(
workflowHistoryUserPreferencesConfig.historyV2ViewEnabled.key,
'true'
);
});

it('should update localStorage when setting value to false in OPT_IN mode', async () => {
const { result, mockSetLocalStorageValue } = setup({
configValue: 'OPT_IN',
localStorageValue: true,
});

await waitFor(() => {
expect(result.current[0]).toBe(true);
});

act(() => {
result.current[1](false);
});

expect(result.current[0]).toBe(false);
expect(mockSetLocalStorageValue).toHaveBeenCalledWith(
workflowHistoryUserPreferencesConfig.historyV2ViewEnabled.key,
'false'
);
});
});

function setup({
configValue,
localStorageValue,
}: {
configValue: 'DISABLED' | 'OPT_IN' | 'OPT_OUT' | 'ENABLED';
localStorageValue?: boolean | null;
}) {
mockUseSuspenseConfigValue.mockReturnValue({
data: configValue,
} satisfies Pick<
UseSuspenseConfigValueResult<'HISTORY_PAGE_V2_ENABLED'>,
'data'
>);

const mockGetLocalStorageValue = jest.fn(() => localStorageValue ?? null);
const mockSetLocalStorageValue = jest.fn();

jest
.spyOn(localStorageModule, 'getLocalStorageValue')
.mockImplementation(mockGetLocalStorageValue);
jest
.spyOn(localStorageModule, 'setLocalStorageValue')
.mockImplementation(mockSetLocalStorageValue);

const { result } = renderHook(
() => useIsWorkflowHistoryV2Enabled(),
undefined,
{
wrapper: ({ children }: { children: React.ReactNode }) => (
<Suspense>{children}</Suspense>
),
}
);

return { result, mockGetLocalStorageValue, mockSetLocalStorageValue };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useCallback, useState } from 'react';

import useSuspenseConfigValue from '@/hooks/use-config-value/use-suspense-config-value';
import {
getLocalStorageValue,
setLocalStorageValue,
} from '@/utils/local-storage';
import workflowHistoryUserPreferencesConfig from '@/views/workflow-history/config/workflow-history-user-preferences.config';

/**
* Manages Workflow History V2 enabled state based on config and localStorage.
*
* @returns A tuple containing:
* - `isWorkflowHistoryV2Enabled`: boolean indicating whether Workflow History V2 is enabled
* - `setIsWorkflowHistoryV2Enabled`: function to update the enabled state
*
* Behavior by config mode:
* - `DISABLED`: Always returns `false`. Setter has no effect.
* - `ENABLED`: Always returns `true`. Setter has no effect.
* - `OPT_OUT`: Always starts with `true`. Setter updates state but does not persist to localStorage.
* - `OPT_IN`: Reads initial state from localStorage (defaults to `false`). Setter updates both state and localStorage.
*/
export default function useIsWorkflowHistoryV2Enabled(): [
boolean,
(v: boolean) => void,
] {
const { data: historyPageV2Config } = useSuspenseConfigValue(
'HISTORY_PAGE_V2_ENABLED'
);

const [isEnabled, setIsEnabled] = useState(() => {
switch (historyPageV2Config) {
case 'DISABLED':
return false;
case 'ENABLED':
case 'OPT_OUT':
return true;
case 'OPT_IN':
const userPreference = getLocalStorageValue(
workflowHistoryUserPreferencesConfig.historyV2ViewEnabled.key,
workflowHistoryUserPreferencesConfig.historyV2ViewEnabled.schema
);
return userPreference ?? false;
}
});

const setIsWorkflowHistoryV2Enabled = useCallback(
(v: boolean) => {
if (
historyPageV2Config === 'DISABLED' ||
historyPageV2Config === 'ENABLED'
) {
return;
}

setIsEnabled(v);

if (historyPageV2Config === 'OPT_IN') {
setLocalStorageValue(
workflowHistoryUserPreferencesConfig.historyV2ViewEnabled.key,
String(v)
);
}
},
[historyPageV2Config]
);

return [isEnabled, setIsWorkflowHistoryV2Enabled];
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ async function setup({
value={{
ungroupedViewUserPreference: ungroupedViewPreference ?? null,
setUngroupedViewUserPreference: mockSetUngroupedViewUserPreference,
isWorkflowHistoryV2Enabled: false,
setIsWorkflowHistoryV2Enabled: jest.fn(),
}}
>
<WorkflowHistory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ const workflowHistoryUserPreferencesConfig = {
.refine((val) => val === 'true' || val === 'false')
.transform((val) => val === 'true'),
},
historyV2ViewEnabled: {
key: 'history-v2-view-enabled',
schema: z
.string()
.refine((val) => val === 'true' || val === 'false')
.transform((val) => val === 'true'),
},
} as const satisfies Record<string, WorkflowHistoryUserPreferenceConfig<any>>;

export default workflowHistoryUserPreferencesConfig;
Loading