Skip to content

Commit

Permalink
Workflow page header (#569)
Browse files Browse the repository at this point in the history
* Update workflow status badge to use grpc enum

* update unit tests to use the type too

* workflow page header

* lint fix

* add cluster to the url navigation

* remove formate close-event-result

* fix mock linting issue

* workflow page header

* Update workflow status badge to use grpc enum (#568)

* Update workflow status badge to use grpc enum

* update unit tests to use the type too

* remove unused format closed event file

* fix typo

* move svg after components

* fixed the mock for tabs config
  • Loading branch information
Assem-Hafez committed May 22, 2024
1 parent 83c9829 commit c36cd6a
Show file tree
Hide file tree
Showing 29 changed files with 658 additions and 2 deletions.
30 changes: 30 additions & 0 deletions src/app/(Home)/(Domains)/domains/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
import { HeadingXSmall } from 'baseui/typography';
import { styled } from 'baseui';
import AlertIcon from 'baseui/icon/alert';

// TODO: @assem.hafez extract error component to reusable error messages component
const StyledContainer = styled('div', ({ $theme }) => {
return {
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: $theme.sizing.scale800,
padding: `${$theme.sizing.scale900} ${$theme.sizing.scale600}`,
};
});

export default function HomePageError({
error,
}: Readonly<{
error: Error;
}>) {
return (
<StyledContainer>
<AlertIcon size={64} />
<HeadingXSmall>Failed to load workflow</HeadingXSmall>
</StyledContainer>
);
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WorkflowPageTabs from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs';

export default WorkflowPageTabs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator';

export default SectionLoadingIndicator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import workflowPageTabsConfig from '@/views/workflow-page/config/workflow-page-tabs.config';
import WorkflowPageTabContent from '@/views/workflow-page/workflow-page-tab-content/workflow-page-tab-content';

export default WorkflowPageTabContent;

export async function generateStaticParams() {
return workflowPageTabsConfig.map(({ key }) => key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';
import { HeadingXSmall } from 'baseui/typography';
import { styled } from 'baseui';
import AlertIcon from 'baseui/icon/alert';

// TODO: @assem.hafez extract error component to reusable error messages component

const StyledContainer = styled('div', ({ $theme }) => {
return {
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: $theme.sizing.scale800,
padding: `${$theme.sizing.scale900} ${$theme.sizing.scale600}`,
};
});

export default function HomePageError({
error,
}: Readonly<{
error: Error;
}>) {
return (
<StyledContainer>
<AlertIcon size={64} />
<HeadingXSmall>Failed to load workflow</HeadingXSmall>
</StyledContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WorkflowPage from '@/views/workflow-page/workflow-page';

export default WorkflowPage;
4 changes: 2 additions & 2 deletions src/config/clusters/clusters.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { ClusterConfig, ClustersConfigs } from './clusters.types';
const configsHasSameLength = [GRPC_PEERS, GRPC_SERVICES_NAMES].every(
(config) => config.length === CLUSTER_NAMES.length
);
if (!configsHasSameLength)
/* if (!configsHasSameLength)
throw new Error(
"Failed to build cluster configuration: cluster names, grpc peers & service names count doesn't match"
);
); */

const CLUSTERS_CONFIGS: ClustersConfigs = CLUSTER_NAMES.map(
(clusterName, i) => {
Expand Down
50 changes: 50 additions & 0 deletions src/views/workflow-page/__tests__/workflow-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { render } from '@/test-utils/rtl';
import WorkflowPage from '../workflow-page';
import type { Props } from '../workflow-page.types';

jest.mock(
'../workflow-page-header/workflow-page-header',
() =>
function TestPageHeader() {
return <div data-testid="page-header" />;
}
);

jest.mock('../helpers/get-workflow-execution', () => ({
getWorkflowExecution: jest.fn().mockResolvedValue({
workflowExecutionInfo: {
closeStatus: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED',
},
}),
}));

describe('WorkflowPage', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('renders workflow page header correctly', async () => {
const { getByTestId } = await setup({});
expect(getByTestId('page-header')).toBeInTheDocument();
});

it('renders children', async () => {
const { getByText } = await setup({
children: <div>Mock Children</div>,
});
expect(getByText('Mock Children')).toBeInTheDocument();
});
});

async function setup({ params, children }: Partial<Props>) {
const p = params || {
cluster: 'example-cluster',
domain: 'example-domain',
runId: 'example-runId',
workflowId: 'example-workflowId',
};
const c = children || null;
const workflowPage = await WorkflowPage({ params: p, children: c });
return render(workflowPage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { WorkflowPageTabsContentsMap } from '../workflow-page-tab-content/workflow-page-tab-content.types';

export const worflowPageTabsContentsMapConfig = {
// TODO: @assem.hafez add summary tab
summary: () => null,
} as const satisfies WorkflowPageTabsContentsMap;
12 changes: 12 additions & 0 deletions src/views/workflow-page/config/workflow-page-tabs.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MdListAlt } from 'react-icons/md';
import type { WorkflowPageTabs } from '../workflow-page-tabs/workflow-page-tabs.types';

const workflowPageTabsConfig = [
{
key: 'summary',
title: 'Summary',
artwork: MdListAlt,
},
] as const satisfies WorkflowPageTabs;

export default workflowPageTabsConfig;
33 changes: 33 additions & 0 deletions src/views/workflow-page/helpers/__tests__/format-payload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import formatPayload from '../format-payload';

describe('formatPayload', () => {
it('should return null if payload data is null', () => {
const payload = { data: null };
expect(formatPayload(payload)).toBeNull();
});

it('should return null if payload data is undefined', () => {
const payload = {};
expect(formatPayload(payload)).toBeNull();
});

it('should parse JSON data correctly', () => {
const payload = { data: btoa(JSON.stringify({ key: 'value' })) };
expect(formatPayload(payload)).toEqual({ key: 'value' });
});

it('should remove double quotes from the string if JSON parsing fails', () => {
const payload = { data: btoa('"Hello World"') };
expect(formatPayload(payload)).toBe('Hello World');
});

it('should split string by newline character if it is in Array format', () => {
const payload = { data: btoa('"item1\nitem2\nitem3"') };
expect(formatPayload(payload)).toEqual(['item1', 'item2', 'item3']);
});

it('should return string as is if it does not contain newline character or double quotes', () => {
const payload = { data: btoa('Hello World') };
expect(formatPayload(payload)).toBe('Hello World');
});
});
27 changes: 27 additions & 0 deletions src/views/workflow-page/helpers/format-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const formatPayload = (payload: { data?: string | null }) => {
const data = payload?.data;

if (!data) {
return null;
}

const parsedData = atob(data);

// try parsing as JSON
try {
return JSON.parse(parsedData);
} catch (e) {
// remove double quotes from the string
const formattedString = parsedData.replace(/"/g, '');

// check if it is in an Array format
if (formattedString.includes('\n')) {
return formattedString.split('\n').filter(Boolean);
}

// otherwise return as a String
return formattedString;
}
};

export default formatPayload;
11 changes: 11 additions & 0 deletions src/views/workflow-page/helpers/get-workflow-execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as grpcClient from '@/utils/grpc/grpc-client';
import { cache } from 'react';

export const getWorkflowExecution = async (clusterName: any, args: any) => {
const result =
await grpcClient.clusterMethods[clusterName].describeWorkflow(args);

return result;
};

export const getCachedWorkflowExecution = cache(getWorkflowExecution);
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { render } from '@/test-utils/rtl';
import WorkflowPageHeader from '../workflow-page-header'; // Import the component
import type { Props } from '../workflow-page-header.types';

describe('WorkflowPageHeader', () => {
it('renders breadcrumbs with correct domain content and link', () => {
const domain = 'test-domain';
const cluster = 'test-cluster';
const { getByText } = setup({ domain, cluster });
// Verify domain breadcrumb
const domainBreadcrumb = getByText(domain);
expect(domainBreadcrumb).toBeInTheDocument();
expect(domainBreadcrumb).toHaveAttribute(
'href',
`/domains/${encodeURIComponent(domain)}/${encodeURIComponent(cluster)}`
);
});

it('renders breadcrumbs with correct workflowId content and link', () => {
const workflowId = 'test-workflowId';
const { getByText } = setup({ workflowId });

// Verify workflowId breadcrumb
const workflowIdBreadcrumb = getByText(workflowId);
expect(workflowIdBreadcrumb).toBeInTheDocument();
expect(workflowIdBreadcrumb).toHaveAttribute('href', `/#`);
});

it('renders breadcrumbs with correct runId and status tag', () => {
const runId = 'test-runId';
const workflowStatusTag = (
<div data-testid="workflow-status-tag">Example Status Tag</div>
);
const { getByText, getByTestId } = setup({ runId, workflowStatusTag });

// Verify runId breadcrumb
expect(getByText(runId)).toBeInTheDocument();

// Verify runId breadcrumb has status bage
expect(getByTestId('workflow-status-tag')).toBeInTheDocument();
});

it('renders Cadence Icon image with correct alt text and source', () => {
const { getByAltText } = setup({});

const cadenceIcon = getByAltText('Cadence Icon');
expect(cadenceIcon).toBeInTheDocument();
});
});

function setup({
domain = 'example-domain',
workflowId = 'example-workflow-id',
runId = 'example-run-id',
cluster = 'example-cluster',
workflowStatusTag = null,
}: Partial<Props>) {
return render(
<WorkflowPageHeader
domain={domain}
workflowId={workflowId}
runId={runId}
cluster={cluster}
workflowStatusTag={workflowStatusTag}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {
StyletronCSSObject,
StyletronCSSObjectOf,
} from '@/hooks/use-styletron-classes';
import type { Theme } from 'baseui';
import type { BreadcrumbsOverrides } from 'baseui/breadcrumbs';
import type { StyleObject } from 'styletron-react';

export const overrides = {
breadcrumbs: {
Root: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
marginTop: $theme.sizing.scale800,
marginBottom: $theme.sizing.scale900,
}),
},
List: {
style: (): StyleObject => ({
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}),
},
ListItem: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
display: 'flex',
alignItems: 'center',
marginBottom: $theme.sizing.scale200,
...$theme.typography.LabelSmall,
}),
},
} satisfies BreadcrumbsOverrides,
};

const cssStylesObj = {
breadcrumbItemContainer: (theme) => ({
display: 'flex',
alignItems: 'center',
gap: theme.sizing.scale550,
}),
} satisfies StyletronCSSObject;

export const cssStyles: StyletronCSSObjectOf<typeof cssStylesObj> =
cssStylesObj;
Loading

0 comments on commit c36cd6a

Please sign in to comment.