Skip to content

Commit

Permalink
Switch basic and advanced workflows base on a flag (#818)
Browse files Browse the repository at this point in the history
* Switch basic and advanced workflows base on a flag

* lint fix

* add spaces between sections

* fix typos and remove unimportant type

* fix lint
  • Loading branch information
Assem-Uber committed Feb 17, 2025
1 parent eb5e94c commit 3d13fe4
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 3 deletions.
18 changes: 18 additions & 0 deletions src/app/api/clusters/[cluster]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type NextRequest } from 'next/server';

import { describeCluster } from '@/route-handlers/describe-cluster/describe-cluster';
import type { RouteParams } from '@/route-handlers/describe-domain/describe-domain.types';
import { routeHandlerWithMiddlewares } from '@/utils/route-handlers-middleware';
import routeHandlersDefaultMiddlewares from '@/utils/route-handlers-middleware/config/route-handlers-default-middlewares.config';

export async function GET(
request: NextRequest,
options: { params: RouteParams }
) {
return routeHandlerWithMiddlewares(
describeCluster,
request,
options,
routeHandlersDefaultMiddlewares
);
}
110 changes: 110 additions & 0 deletions src/route-handlers/describe-cluster/__tests__/describe-cluster.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { NextRequest } from 'next/server';

import type { DescribeClusterResponse as OriginalDescribeClusterResponse } from '@/__generated__/proto-ts/uber/cadence/admin/v1/DescribeClusterResponse';
import { GRPCError } from '@/utils/grpc/grpc-error';
import { mockGrpcClusterMethods } from '@/utils/route-handlers-middleware/middlewares/__mocks__/grpc-cluster-methods';

import { describeCluster } from '../describe-cluster';
import {
type DescribeClusterResponse,
type Context,
} from '../describe-cluster.types';

describe('describeCluster', () => {
it('calls describeCluster and returns valid response without membershipInfo', async () => {
const { res, mockDescribeCluster, mockSuccessResponse } = await setup({});

expect(mockDescribeCluster).toHaveBeenCalledWith({
name: 'mock-cluster',
});
const { membershipInfo, ...rest } = mockSuccessResponse;
const routHandleRes: DescribeClusterResponse = rest;
const responseJson = await res.json();
expect(responseJson).toEqual(routHandleRes);
});

it('returns an error when describeCluster errors out', async () => {
const { res, mockDescribeCluster } = await setup({
error: true,
});

expect(mockDescribeCluster).toHaveBeenCalled();

expect(res.status).toEqual(500);
const responseJson = await res.json();
expect(responseJson).toEqual(
expect.objectContaining({
message: 'Failed to fetch cluster info',
})
);
});

it('returns static response with advancedVisibilityEnabled true when CADENCE_ADVANCED_VISIBILITY env value is true', async () => {
const originalEnvObj = globalThis.process.env;
globalThis.process.env = {
...process.env,
CADENCE_ADVANCED_VISIBILITY: 'true',
};

const { res, mockDescribeCluster } = await setup({});

expect(mockDescribeCluster).not.toHaveBeenCalled();

const responseJson = await res.json();
expect(
responseJson.persistenceInfo.visibilityStore.features[0].enabled
).toBe(true);
globalThis.process.env = originalEnvObj;
});

it('returns static response with advancedVisibilityEnabled false when CADENCE_ADVANCED_VISIBILITY env value is not true', async () => {
const originalEnvObj = globalThis.process.env;
globalThis.process.env = {
...process.env,
CADENCE_ADVANCED_VISIBILITY: 'not true',
};

const { res, mockDescribeCluster } = await setup({});

expect(mockDescribeCluster).not.toHaveBeenCalled();

const responseJson = await res.json();
expect(
responseJson.persistenceInfo.visibilityStore.features[0].enabled
).toBe(false);
globalThis.process.env = originalEnvObj;
});
});

async function setup({ error }: { error?: true }) {
const mockSuccessResponse: OriginalDescribeClusterResponse = {
persistenceInfo: {},
membershipInfo: null,
supportedClientVersions: null,
};

const mockDescribeCluster = jest
.spyOn(mockGrpcClusterMethods, 'describeCluster')
.mockImplementationOnce(async () => {
if (error) {
throw new GRPCError('Failed to fetch cluster info');
}
return mockSuccessResponse;
});

const res = await describeCluster(
new NextRequest('http://localhost/api/clusters/:cluster', {
method: 'Get',
}),
{
params: {
cluster: 'mock-cluster',
},
},
{
grpcClusterMethods: mockGrpcClusterMethods,
} as Context
);

return { res, mockDescribeCluster, mockSuccessResponse };
}
63 changes: 63 additions & 0 deletions src/route-handlers/describe-cluster/describe-cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import omit from 'lodash/omit';
import { type NextRequest, NextResponse } from 'next/server';

import decodeUrlParams from '@/utils/decode-url-params';
import { getHTTPStatusCode, GRPCError } from '@/utils/grpc/grpc-error';
import logger, { type RouteHandlerErrorPayload } from '@/utils/logger';

import {
type DescribeClusterResponse,
type Context,
type RequestParams,
type RouteParams,
} from './describe-cluster.types';

export async function describeCluster(
_: NextRequest,
requestParams: RequestParams,
ctx: Context
) {
const decodedParams = decodeUrlParams(requestParams.params) as RouteParams;

// temporary solution to disable invoking describeCluster
if (process.env.CADENCE_ADVANCED_VISIBILITY) {
const res = {
persistenceInfo: {
visibilityStore: {
features: [
{
key: 'advancedVisibilityEnabled',
enabled: process.env.CADENCE_ADVANCED_VISIBILITY === 'true',
},
],
},
},
supportedClientVersions: null,
};
return NextResponse.json(res);
}

try {
const res = await ctx.grpcClusterMethods.describeCluster({
name: decodedParams.cluster,
});

const sanitizedRes: DescribeClusterResponse = omit(res, 'membershipInfo'); // No need to return host information to client

return NextResponse.json(sanitizedRes);
} catch (e) {
logger.error<RouteHandlerErrorPayload>(
{ requestParams: decodedParams, cause: e },
'Error fetching cluster info'
);

return NextResponse.json(
{
message:
e instanceof GRPCError ? e.message : 'Error fetching cluster info',
cause: e,
},
{ status: getHTTPStatusCode(e) }
);
}
}
16 changes: 16 additions & 0 deletions src/route-handlers/describe-cluster/describe-cluster.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type DescribeClusterResponse as OriginalDescribeClusterResponse } from '@/__generated__/proto-ts/uber/cadence/admin/v1/DescribeClusterResponse';
import { type DefaultMiddlewaresContext } from '@/utils/route-handlers-middleware';

export type RouteParams = {
cluster: string;
};

export type RequestParams = {
params: RouteParams;
};

export type Context = DefaultMiddlewaresContext;
export type DescribeClusterResponse = Omit<
OriginalDescribeClusterResponse,
'membershipInfo'
>;
105 changes: 105 additions & 0 deletions src/views/domain-workflows/__tests__/domain-workflows.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Suspense } from 'react';

import { HttpResponse } from 'msw';
import { act } from 'react-dom/test-utils';

import { render, screen } from '@/test-utils/rtl';

import { type DescribeClusterResponse } from '@/route-handlers/describe-cluster/describe-cluster.types';
import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types';

import DomainWorkflows from '../domain-workflows';

jest.mock('@/views/domain-workflows-basic/domain-workflows-basic', () =>
jest.fn(() => <div>Basic Workflows</div>)
);
jest.mock('../domain-workflows-header/domain-workflows-header', () =>
jest.fn(() => <div>Workflows Header</div>)
);
jest.mock('../domain-workflows-table/domain-workflows-table', () =>
jest.fn(() => <div>Workflows Table</div>)
);

describe('DomainWorkflows', () => {
it('should render basic workflows when advanced visibility is disabled', async () => {
await setup({ isAdvancedVisibility: false });

expect(await screen.findByText('Basic Workflows')).toBeInTheDocument();
});

it('should render workflows header and table when advanced visibility is enabled', async () => {
await setup({ isAdvancedVisibility: true });

expect(await screen.findByText('Workflows Header')).toBeInTheDocument();
expect(await screen.findByText('Workflows Table')).toBeInTheDocument();
});

it('should render workflows header and table when advanced visibility is enabled', async () => {
let renderErrorMessage;
try {
await act(async () => {
await setup({ error: true });
});
} catch (error) {
if (error instanceof Error) {
renderErrorMessage = error.message;
}
}

expect(renderErrorMessage).toEqual('Failed to fetch cluster info');
});
});

async function setup({
isAdvancedVisibility = false,
error,
}: {
error?: boolean;
isAdvancedVisibility?: boolean;
}) {
const props: DomainPageTabContentProps = {
domain: 'test-domain',
cluster: 'test-cluster',
};

render(
<Suspense>
<DomainWorkflows {...props} />
</Suspense>,
{
endpointsMocks: [
{
path: '/api/clusters/test-cluster',
httpMethod: 'GET',
mockOnce: false,
...(error
? {
httpResolver: () => {
return HttpResponse.json(
{ message: 'Failed to fetch cluster info' },
{ status: 500 }
);
},
}
: {
jsonResponse: {
persistenceInfo: {
visibilityStore: {
features: [
{
key: 'advancedVisibilityEnabled',
enabled: isAdvancedVisibility,
},
],
backend: '',
settings: [],
},
},
supportedClientVersions: null,
} satisfies DescribeClusterResponse,
}),
},
],
}
);
}
38 changes: 35 additions & 3 deletions src/views/domain-workflows/domain-workflows.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import React from 'react';
import React, { useMemo } from 'react';

import { useSuspenseQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';

import { type DescribeClusterResponse } from '@/route-handlers/describe-cluster/describe-cluster.types';
import request from '@/utils/request';
import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types';

import DomainWorkflowsHeader from './domain-workflows-header/domain-workflows-header';
import DomainWorkflowsTable from './domain-workflows-table/domain-workflows-table';
import isClusterAdvancedVisibilityEnabled from './helpers/is-cluster-advanced-visibility-enabled';

const DomainWorkflowsBasic = dynamic(
() => import('@/views/domain-workflows-basic/domain-workflows-basic')
);

const DomainWorkflowsHeader = dynamic(
() => import('./domain-workflows-header/domain-workflows-header')
);

const DomainWorkflowsTable = dynamic(
() => import('./domain-workflows-table/domain-workflows-table')
);

export default function DomainWorkflows(props: DomainPageTabContentProps) {
const { data } = useSuspenseQuery<DescribeClusterResponse>({
queryKey: ['describeCluster', props],
queryFn: () =>
request(`/api/clusters/${props.cluster}`).then((res) => res.json()),
});

const isAdvancedVisibilityEnabled = useMemo(() => {
return isClusterAdvancedVisibilityEnabled(data);
}, [data]);

if (!isAdvancedVisibilityEnabled) {
return (
<DomainWorkflowsBasic domain={props.domain} cluster={props.cluster} />
);
}

return (
<>
<DomainWorkflowsHeader domain={props.domain} cluster={props.cluster} />
Expand Down
Loading

0 comments on commit 3d13fe4

Please sign in to comment.