diff --git a/pages/_app.tsx b/pages/_app.tsx index af300ed..c0734ae 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -8,8 +8,10 @@ import { SessionProvider } from 'next-auth/react'; import { NextAdapter } from 'next-query-params'; import App, { AppContext, AppInitialProps, AppProps } from 'next/app'; import Head from 'next/head'; +import { useRouter } from 'next/router'; import { QueryParamProvider } from 'use-query-params'; +import type { EuiSideNavItemType } from '@elastic/eui'; import { EuiProvider, EuiThemeColorMode } from '@elastic/eui'; import '@elastic/eui/dist/eui_theme_dark.min.css'; import '@elastic/eui/dist/eui_theme_light.min.css'; @@ -49,6 +51,7 @@ function CustomApp({ pageProps, orchestratorConfig, }: AppProps & AppOwnProps) { + const router = useRouter(); const [queryClient] = useState(() => new QueryClient(queryClientConfig)); const [themeMode, setThemeMode] = useState( @@ -72,6 +75,124 @@ function CustomApp({ } }, []); + const getMenuItems = (): EuiSideNavItemType[] => { + return [ + { + name: 'Start', + id: '2', + isSelected: router.pathname === '/', + onClick: (e) => { + e.preventDefault(); + router.push('/'); + }, + }, + { + name: 'Routines', + id: '3', + isSelected: router.pathname === '/custom/routines', + href: '/custom/routines', + onClick: (e) => { + e.preventDefault(); + router.push('/custom/routines'); + }, + }, + { + name: 'Notifications', + id: '4', + isSelected: router.pathname === '/custom/notifications', + href: '/custom/notifications', + onClick: (e) => { + e.preventDefault(); + router.push('/custom/notifications'); + }, + }, + { + name: 'Configuration', + id: '5', + href: '/custom/configuration/custom-fixed-version-types', + onClick: () => { + router.push( + '/custom/configuration/custom-fixed-version-types', + ); + }, + items: [ + { + name: 'Fixed Version Types', + id: '5.1', + isSelected: + router.pathname === + '/custom/configuration/custom-fixed-version-types', + onClick: (e) => { + e.preventDefault(); + router.push( + '/custom/configuration/custom-fixed-version-types', + ); + }, + }, + { + name: 'Product blocks', + id: '5.2', + isSelected: + router.pathname === + '/custom/configuration/custom-product-blocks', + onClick: (e) => { + e.preventDefault(); + router.push( + '/custom/configuration/custom-product-blocks', + ); + }, + }, + { + name: 'Products', + id: '5.3', + isSelected: + router.pathname === + '/custom/configuration/custom-products', + onClick: (e) => { + e.preventDefault(); + router.push( + '/custom/configuration/custom-products', + ); + }, + }, + { + name: 'Routines', + id: '5.4', + isSelected: + router.pathname === + '/custom/configuration/custom-routines', + onClick: (e) => { + e.preventDefault(); + router.push( + '/custom/configuration/custom-routines', + ); + }, + }, + ], + }, + { + name: 'Jobs', + isSelected: router.pathname === '/custom/jobs', + id: '6', + onClick: (e) => { + e.preventDefault(); + router.push('/custom/jobs'); + }, + href: '/custom/jobs', + }, + { + name: 'Settings', + isSelected: router.pathname === '/custom/custom-settings', + id: '7', + onClick: (e) => { + e.preventDefault(); + router.push('/custom/custom-settings'); + }, + href: '/custom/custom-settings', + }, + ]; + }; + return ( >(); + + const getStoredTableConfig = useStoredTableConfig( + METADATA_RESOURCE_TYPES_TABLE_LOCAL_STORAGE_KEY, + ); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // the fist time before the useEffect has populated the tableDefaults. Better is to create a way for + // the query to wait for the values to be available + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: RESOURCE_TYPE_FIELD_TYPE, + order: SortOrder.ASC, + }, + }); + + const tableColumns: WfoTableColumns = { + resourceTypeId: { + field: RESOURCE_TYPE_FIELD_ID, + name: t('resourceId'), + width: '90', + render: (value) => , + renderDetails: (value) => value, + }, + resourceType: { + field: RESOURCE_TYPE_FIELD_TYPE, + name: t('type'), + width: '200', + render: (value) => ( + + {value} + + ), + }, + description: { + field: RESOURCE_TYPE_FIELD_DESCRIPTION, + name: t('description'), + }, + productBlocks: { + field: RESOURCE_TYPE_FIELD_PRODUCT_BLOCKS, + name: t('usedInProductBlocks'), + render: (productBlocks) => ( + <> + {productBlocks.map((productBlock, index) => ( + + {productBlock.name} + + ))} + + ), + renderDetails: (productBlocks) => ( + + {productBlocks.map((productBlock, index) => ( + + {productBlock.name} + + ))} + + ), + }, + }; + + const { pageSize, pageIndex, sortBy, queryString } = dataDisplayParams; + const graphqlQueryVariables: GraphqlQueryVariables = + { + first: pageSize, + after: pageIndex * pageSize, + sortBy: sortBy, + query: queryString || undefined, + }; + const { data, isFetching, isError } = useGetResourceTypesQuery( + graphqlQueryVariables, + ); + + const [getResourceTypesTrigger, { isFetching: isFetchingCsv }] = + useLazyGetResourceTypesQuery(); + + const getResourceTypesForExport = () => + getResourceTypesTrigger( + getQueryVariablesForExport(graphqlQueryVariables), + ).unwrap(); + + const dataSorting: WfoDataSorting = { + field: sortBy?.field ?? RESOURCE_TYPE_FIELD_TYPE, + sortOrder: sortBy?.order ?? SortOrder.ASC, + }; + + const { totalItems, sortFields, filterFields } = data?.pageInfo || {}; + + const pagination: Pagination = { + pageSize: pageSize, + pageIndex: pageIndex, + pageSizeOptions: DEFAULT_PAGE_SIZES, + totalItemCount: totalItems ? totalItems : 0, + }; + + return ( + + + data={data ? data.resourceTypes : []} + tableColumns={mapSortableAndFilterableValuesToTableColumnConfig( + tableColumns, + sortFields, + filterFields, + )} + dataSorting={dataSorting} + defaultHiddenColumns={tableDefaults?.hiddenColumns} + onUpdateDataSort={getDataSortHandler( + setDataDisplayParam, + )} + onUpdatePage={getPageChangeHandler( + setDataDisplayParam, + )} + onUpdateQueryString={getQueryStringHandler( + setDataDisplayParam, + )} + pagination={pagination} + isLoading={isFetching} + hasError={isError} + queryString={queryString} + localStorageKey={ + METADATA_RESOURCE_TYPES_TABLE_LOCAL_STORAGE_KEY + } + onExportData={csvDownloadHandler( + getResourceTypesForExport, + (data) => data.resourceTypes, + (data) => data.pageInfo, + Object.keys(tableColumns), + getCsvFileNameWithDate('ResourceTypes'), + showToastMessage, + tError, + )} + exportDataIsLoading={isFetchingCsv} + /> + + ); +} diff --git a/pages/custom/configuration/custom-product-blocks.tsx b/pages/custom/configuration/custom-product-blocks.tsx new file mode 100644 index 0000000..70e7de9 --- /dev/null +++ b/pages/custom/configuration/custom-product-blocks.tsx @@ -0,0 +1,259 @@ +import React, { useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; + +import { EuiBadgeGroup } from '@elastic/eui'; +import type { Pagination } from '@elastic/eui/src/components'; +import { + DEFAULT_PAGE_SIZE, + DEFAULT_PAGE_SIZES, + METADATA_PRODUCT_BLOCKS_TABLE_LOCAL_STORAGE_KEY, + StoredTableConfig, + WfoDataSorting, + WfoDateTime, + WfoFirstPartUUID, + WfoProductBlockBadge, + WfoProductStatusBadge, + WfoTableColumns, + WfoTableWithFilter, + getDataSortHandler, + getPageChangeHandler, + getQueryStringHandler, + mapSortableAndFilterableValuesToTableColumnConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + useDataDisplayParams, + useShowToastMessage, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + useGetProductBlocksQuery, + useLazyGetProductBlocksQuery, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + BadgeType, + GraphqlQueryVariables, + ProductBlockDefinition, + SortOrder, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + csvDownloadHandler, + getCsvFileNameWithDate, + getQueryVariablesForExport, + parseDateToLocaleDateTimeString, + parseIsoString, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { WfoMetadataPageLayout } from '@orchestrator-ui/orchestrator-ui-components'; + +import { configurationTabs } from './configurationTabs'; + +const PRODUCT_BLOCK_FIELD_ID = 'productBlockId'; +const PRODUCT_BLOCK_FIELD_NAME = 'name'; +const PRODUCT_BLOCK_FIELD_TAG = 'tag'; +const PRODUCT_BLOCK_FIELD_DESCRIPTION = 'description'; +const PRODUCT_BLOCK_FIELD_STATUS = 'status'; +const PRODUCT_BLOCK_FIELD_CREATED_AT = 'createdAt'; +const PRODUCT_BLOCK_FIELD_END_DATE = 'endDate'; +const PRODUCT_BLOCK_FIELD_RESOURCE_TYPES = 'resourceTypes'; +const PRODUCT_BLOCK_FIELD_PRODUCT_BLOCKS = 'dependsOn'; + +export default function WfoProductBlocksPage() { + const t = useTranslations('configurations.productBlocks'); + const tError = useTranslations('errors'); + const { showToastMessage } = useShowToastMessage(); + + const [tableDefaults, setTableDefaults] = + useState>(); + + const getStoredTableConfig = useStoredTableConfig( + METADATA_PRODUCT_BLOCKS_TABLE_LOCAL_STORAGE_KEY, + ); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // the fist time before the useEffect has populated the tableDefaults. Better is to create a way for + // the query to wait for the values to be available + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: PRODUCT_BLOCK_FIELD_NAME, + order: SortOrder.ASC, + }, + }); + + const tableColumns: WfoTableColumns = { + productBlockId: { + field: PRODUCT_BLOCK_FIELD_ID, + name: t('id'), + width: '90', + render: (value) => , + renderDetails: (value) => value, + }, + name: { + field: PRODUCT_BLOCK_FIELD_NAME, + name: t('name'), + width: '200', + render: (name) => ( + + {name} + + ), + }, + tag: { + field: PRODUCT_BLOCK_FIELD_TAG, + name: t('tag'), + width: '140', + }, + description: { + field: PRODUCT_BLOCK_FIELD_DESCRIPTION, + name: t('description'), + width: '400', + }, + status: { + field: PRODUCT_BLOCK_FIELD_STATUS, + name: t('status'), + width: '90', + render: (value) => , + }, + dependsOn: { + field: PRODUCT_BLOCK_FIELD_PRODUCT_BLOCKS, + name: t('dependingProductBlocks'), + render: (dependsOn) => ( + <> + {dependsOn.map((productBlock, index) => ( + + {productBlock.name} + + ))} + + ), + }, + resourceTypes: { + field: PRODUCT_BLOCK_FIELD_RESOURCE_TYPES, + name: t('resourceTypes'), + render: (resourceTypes) => ( + <> + {resourceTypes.map((resourceType, index) => ( + + {resourceType.resourceType} + + ))} + + ), + renderDetails: (resourceTypes) => ( + + {resourceTypes.map((resourceType, index) => ( + + {resourceType.resourceType} + + ))} + + ), + }, + createdAt: { + field: PRODUCT_BLOCK_FIELD_CREATED_AT, + name: t('createdAt'), + render: (date) => , + renderDetails: parseIsoString(parseDateToLocaleDateTimeString), + clipboardText: parseIsoString(parseDateToLocaleDateTimeString), + }, + endDate: { + field: PRODUCT_BLOCK_FIELD_END_DATE, + name: t('endDate'), + render: (date) => , + renderDetails: parseIsoString(parseDateToLocaleDateTimeString), + clipboardText: parseIsoString(parseDateToLocaleDateTimeString), + }, + }; + + const { pageSize, pageIndex, sortBy, queryString } = dataDisplayParams; + const graphqlQueryVariables: GraphqlQueryVariables = + { + first: pageSize, + after: pageIndex * pageSize, + sortBy: sortBy, + query: queryString || undefined, + }; + const { data, isFetching, isError } = useGetProductBlocksQuery( + graphqlQueryVariables, + ); + const [getProductBlocksTrigger, { isFetching: isFetchingCsv }] = + useLazyGetProductBlocksQuery(); + const getProductBlocksForExport = () => + getProductBlocksTrigger( + getQueryVariablesForExport(graphqlQueryVariables), + ).unwrap(); + + const dataSorting: WfoDataSorting = { + field: sortBy?.field ?? PRODUCT_BLOCK_FIELD_NAME, + sortOrder: sortBy?.order ?? SortOrder.ASC, + }; + + const { totalItems, sortFields, filterFields } = data?.pageInfo ?? {}; + + const pagination: Pagination = { + pageSize: pageSize, + pageIndex: pageIndex, + pageSizeOptions: DEFAULT_PAGE_SIZES, + totalItemCount: totalItems ? totalItems : 0, + }; + + return ( + + + data={data?.productBlocks || []} + tableColumns={mapSortableAndFilterableValuesToTableColumnConfig( + tableColumns, + sortFields, + filterFields, + )} + dataSorting={dataSorting} + defaultHiddenColumns={tableDefaults?.hiddenColumns} + onUpdateDataSort={getDataSortHandler( + setDataDisplayParam, + )} + onUpdatePage={getPageChangeHandler( + setDataDisplayParam, + )} + onUpdateQueryString={getQueryStringHandler( + setDataDisplayParam, + )} + pagination={pagination} + isLoading={isFetching} + hasError={isError} + queryString={queryString} + localStorageKey={ + METADATA_PRODUCT_BLOCKS_TABLE_LOCAL_STORAGE_KEY + } + onExportData={csvDownloadHandler( + getProductBlocksForExport, + (data) => data.productBlocks, + (data) => data.pageInfo, + Object.keys(tableColumns), + getCsvFileNameWithDate('ProductBlocks'), + showToastMessage, + tError, + )} + exportDataIsLoading={isFetchingCsv} + /> + + ); +} diff --git a/pages/custom/configuration/custom-products.tsx b/pages/custom/configuration/custom-products.tsx new file mode 100644 index 0000000..bea346d --- /dev/null +++ b/pages/custom/configuration/custom-products.tsx @@ -0,0 +1,245 @@ +import React, { useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; + +import type { Pagination } from '@elastic/eui/src/components'; +import type { + WfoDataSorting, + WfoTableColumns, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + DEFAULT_PAGE_SIZE, + DEFAULT_PAGE_SIZES, + METADATA_PRODUCT_TABLE_LOCAL_STORAGE_KEY, + StoredTableConfig, + WfoDateTime, + WfoProductBlockBadge, + WfoProductStatusBadge, + WfoTableWithFilter, + getDataSortHandler, + getPageChangeHandler, + getQueryStringHandler, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { WfoFirstPartUUID } from '@orchestrator-ui/orchestrator-ui-components'; +import { mapSortableAndFilterableValuesToTableColumnConfig } from '@orchestrator-ui/orchestrator-ui-components'; +import { + useDataDisplayParams, + useShowToastMessage, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + useGetProductsQuery, + useLazyGetProductsQuery, +} from '@orchestrator-ui/orchestrator-ui-components'; +import type { + GraphqlQueryVariables, + ProductDefinition, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + BadgeType, + SortOrder, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + getQueryVariablesForExport, + parseDateToLocaleDateTimeString, + parseIsoString, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + csvDownloadHandler, + getCsvFileNameWithDate, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { WfoMetadataPageLayout } from '@orchestrator-ui/orchestrator-ui-components'; + +import { configurationTabs } from './configurationTabs'; + +const PRODUCT_FIELD_PRODUCT_ID = 'productId'; +const PRODUCT_FIELD_NAME = 'name'; +const PRODUCT_FIELD_DESCRIPTION = 'description'; +const PRODUCT_FIELD_TAG = 'tag'; +const PRODUCT_FIELD_PRODUCT_TYPE = 'productType'; +const PRODUCT_FIELD_STATUS = 'status'; +const PRODUCT_FIELD_PRODUCT_BLOCKS = 'productBlocks'; +const PRODUCT_FIELD_FIXED_INPUTS = 'fixedInputs'; +const PRODUCT_FIELD_CREATED_AT = 'createdAt'; + +export default function WfoProductsPage() { + const t = useTranslations('configurations.products'); + const tError = useTranslations('errors'); + const { showToastMessage } = useShowToastMessage(); + const [tableDefaults, setTableDefaults] = + useState>(); + + const getStoredTableConfig = useStoredTableConfig( + METADATA_PRODUCT_TABLE_LOCAL_STORAGE_KEY, + ); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // the fist time before the useEffect has populated the tableDefaults. Better is to create a way for + // the query to wait for the values to be available + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: PRODUCT_FIELD_NAME, + order: SortOrder.ASC, + }, + }); + + const tableColumns: WfoTableColumns = { + productId: { + field: PRODUCT_FIELD_PRODUCT_ID, + name: t('id'), + width: '90', + render: (value) => , + renderDetails: (value) => value, + }, + name: { + field: PRODUCT_FIELD_NAME, + name: t('name'), + width: '200', + }, + tag: { + field: PRODUCT_FIELD_TAG, + name: t('tag'), + width: '120', + render: (value) => ( + + {value} + + ), + }, + description: { + field: PRODUCT_FIELD_DESCRIPTION, + name: t('description'), + width: '400', + }, + productType: { + field: PRODUCT_FIELD_PRODUCT_TYPE, + name: t('productType'), + }, + status: { + field: PRODUCT_FIELD_STATUS, + name: t('status'), + width: '90', + render: (value) => , + }, + fixedInputs: { + field: PRODUCT_FIELD_FIXED_INPUTS, + name: t('fixedInputs'), + render: (fixedInputs) => ( + <> + {fixedInputs.map((fixedInput, index) => ( + + {`${fixedInput.name}: ${fixedInput.value}`} + + ))} + + ), + }, + productBlocks: { + field: PRODUCT_FIELD_PRODUCT_BLOCKS, + name: t('productBlocks'), + render: (productBlocks) => ( + <> + {productBlocks.map((block, index) => ( + + {block.name} + + ))} + + ), + }, + createdAt: { + field: PRODUCT_FIELD_CREATED_AT, + name: t('createdAt'), + render: (date) => , + renderDetails: parseIsoString(parseDateToLocaleDateTimeString), + clipboardText: parseIsoString(parseDateToLocaleDateTimeString), + }, + }; + + const { pageSize, pageIndex, sortBy, queryString } = dataDisplayParams; + const graphqlQueryVariables: GraphqlQueryVariables = { + first: pageSize, + after: pageIndex * pageSize, + sortBy: sortBy, + query: queryString || undefined, + }; + const { data, isFetching, isError } = useGetProductsQuery( + graphqlQueryVariables, + ); + const [getProductsTrigger, { isFetching: isFetchingCsv }] = + useLazyGetProductsQuery(); + const getProductsForExport = () => + getProductsTrigger( + getQueryVariablesForExport(graphqlQueryVariables), + ).unwrap(); + + const { totalItems, sortFields, filterFields } = data?.pageInfo ?? {}; + + const pagination: Pagination = { + pageSize: pageSize, + pageIndex: pageIndex, + pageSizeOptions: DEFAULT_PAGE_SIZES, + totalItemCount: totalItems ? totalItems : 0, + }; + + const dataSorting: WfoDataSorting = { + field: sortBy?.field ?? PRODUCT_FIELD_NAME, + sortOrder: sortBy?.order ?? SortOrder.ASC, + }; + + return ( + + + data={data?.products ?? []} + tableColumns={mapSortableAndFilterableValuesToTableColumnConfig( + tableColumns, + sortFields, + filterFields, + )} + dataSorting={dataSorting} + defaultHiddenColumns={tableDefaults?.hiddenColumns} + onUpdateDataSort={getDataSortHandler( + setDataDisplayParam, + )} + onUpdatePage={getPageChangeHandler( + setDataDisplayParam, + )} + onUpdateQueryString={getQueryStringHandler( + setDataDisplayParam, + )} + pagination={pagination} + isLoading={isFetching} + hasError={isError} + queryString={queryString} + localStorageKey={METADATA_PRODUCT_TABLE_LOCAL_STORAGE_KEY} + onExportData={csvDownloadHandler( + getProductsForExport, + (data) => data?.products ?? [], + (data) => data?.pageInfo || {}, + Object.keys(tableColumns), + getCsvFileNameWithDate('Products'), + showToastMessage, + tError, + )} + exportDataIsLoading={isFetchingCsv} + /> + + ); +} diff --git a/pages/custom/configuration/custom-routines.tsx b/pages/custom/configuration/custom-routines.tsx new file mode 100644 index 0000000..55c442b --- /dev/null +++ b/pages/custom/configuration/custom-routines.tsx @@ -0,0 +1,243 @@ +import React, { useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; + +import { EuiBadgeGroup } from '@elastic/eui'; +import type { Pagination } from '@elastic/eui/src/components'; +import type { + WfoDataSorting, + WfoTableColumns, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + DEFAULT_PAGE_SIZE, + DEFAULT_PAGE_SIZES, + METADATA_WORKFLOWS_TABLE_LOCAL_STORAGE_KEY, + StoredTableConfig, + WfoDateTime, + WfoProductBlockBadge, + WfoTableWithFilter, + WfoWorkflowTargetBadge, + getDataSortHandler, + getPageChangeHandler, + getQueryStringHandler, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + useDataDisplayParams, + useShowToastMessage, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + useGetWorkflowsQuery, + useLazyGetWorkflowsQuery, +} from '@orchestrator-ui/orchestrator-ui-components'; +import type { + GraphqlQueryVariables, + WorkflowDefinition, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + BadgeType, + SortOrder, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + getQueryVariablesForExport, + onlyUnique, + parseDateToLocaleDateTimeString, + parseIsoString, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + csvDownloadHandler, + getCsvFileNameWithDate, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { mapSortableAndFilterableValuesToTableColumnConfig } from '@orchestrator-ui/orchestrator-ui-components'; +import { WfoMetadataPageLayout } from '@orchestrator-ui/orchestrator-ui-components'; +import { + graphQlWorkflowListMapper, + mapWorkflowDefinitionToWorkflowListItem, +} from '@orchestrator-ui/orchestrator-ui-components'; + +import { configurationTabs } from './configurationTabs'; + +export type WorkflowListItem = Pick< + WorkflowDefinition, + 'name' | 'description' | 'target' | 'createdAt' +> & { + productTags: string[]; +}; + +export default function RoutinesPage() { + const t = useTranslations('configurations.routines'); + const tError = useTranslations('errors'); + const { showToastMessage } = useShowToastMessage(); + + const [tableDefaults, setTableDefaults] = + useState>(); + + const getStoredTableConfig = useStoredTableConfig( + METADATA_WORKFLOWS_TABLE_LOCAL_STORAGE_KEY, + ); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // the fist time before the useEffect has populated the tableDefaults. Better is to create a way for + // the query to wait for the values to be available + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: 'name', + order: SortOrder.ASC, + }, + }); + + const tableColumns: WfoTableColumns = { + name: { + field: 'name', + name: t('name'), + width: '200', + render: (name) => ( + + {name} + + ), + }, + description: { + field: 'description', + name: t('description'), + width: '300', + }, + target: { + field: 'target', + name: t('target'), + width: '90', + render: (target) => , + }, + productTags: { + field: 'productTags', + name: t('productTags'), + render: (productTags) => ( + <> + {productTags + ?.filter(onlyUnique) + .map((productTag, index) => ( + + {productTag} + + ))} + + ), + renderDetails: (productTags) => ( + + {productTags + ?.filter(onlyUnique) + .map((productTag, index) => ( + + {productTag} + + ))} + + ), + }, + createdAt: { + field: 'createdAt', + name: t('createdAt'), + width: '110', + render: (date) => , + renderDetails: parseIsoString(parseDateToLocaleDateTimeString), + clipboardText: parseIsoString(parseDateToLocaleDateTimeString), + }, + }; + + const { pageSize, pageIndex, sortBy, queryString } = dataDisplayParams; + + const workflowListQueryVariables: GraphqlQueryVariables = + { + first: pageSize, + after: pageIndex * pageSize, + sortBy: graphQlWorkflowListMapper(sortBy), + query: queryString || undefined, + }; + const { data, isFetching, isError } = useGetWorkflowsQuery( + workflowListQueryVariables, + ); + + const [getWorkflowsTrigger, { isFetching: isFetchingCsv }] = + useLazyGetWorkflowsQuery(); + + const getWorkflowsForExport = () => + getWorkflowsTrigger( + getQueryVariablesForExport(workflowListQueryVariables), + ).unwrap(); + + const dataSorting: WfoDataSorting = { + field: sortBy?.field ?? 'name', + sortOrder: sortBy?.order ?? SortOrder.ASC, + }; + + const { totalItems, sortFields, filterFields } = data?.pageInfo || {}; + + const pagination: Pagination = { + pageSize: pageSize, + pageIndex: pageIndex, + pageSizeOptions: DEFAULT_PAGE_SIZES, + totalItemCount: totalItems ? totalItems : 0, + }; + + return ( + + + data={ + data + ? mapWorkflowDefinitionToWorkflowListItem( + data.workflows, + ) + : [] + } + tableColumns={mapSortableAndFilterableValuesToTableColumnConfig( + tableColumns, + sortFields, + filterFields, + )} + dataSorting={dataSorting} + defaultHiddenColumns={tableDefaults?.hiddenColumns} + onUpdateDataSort={getDataSortHandler( + setDataDisplayParam, + )} + onUpdatePage={getPageChangeHandler( + setDataDisplayParam, + )} + onUpdateQueryString={getQueryStringHandler( + setDataDisplayParam, + )} + pagination={pagination} + isLoading={isFetching} + hasError={isError} + queryString={queryString} + localStorageKey={METADATA_WORKFLOWS_TABLE_LOCAL_STORAGE_KEY} + onExportData={csvDownloadHandler( + getWorkflowsForExport, + (data) => data.workflows, + (data) => data.pageInfo, + Object.keys(tableColumns), + getCsvFileNameWithDate('Workflows'), + showToastMessage, + tError, + )} + exportDataIsLoading={isFetchingCsv} + /> + + ); +} diff --git a/pages/custom/custom-settings.tsx b/pages/custom/custom-settings.tsx new file mode 100644 index 0000000..ca2f1f5 --- /dev/null +++ b/pages/custom/custom-settings.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { EuiHorizontalRule, EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { + WfoFlushSettings, + WfoModifySettings, + WfoStatus, +} from '@orchestrator-ui/orchestrator-ui-components'; + +export default function CustomSettingsPage() { + return ( + <> + + + + + + + + + + + ); +} diff --git a/pages/custom/jobs/[jobId].tsx b/pages/custom/jobs/[jobId].tsx new file mode 100644 index 0000000..40f2321 --- /dev/null +++ b/pages/custom/jobs/[jobId].tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { WfoProcessDetailPage } from '@orchestrator-ui/orchestrator-ui-components'; + +export default function JobDetailPage() { + const router = useRouter(); + const { jobId } = router.query; + + return ( + <> + {(jobId && typeof jobId === 'string' && ( + + )) ||
Invalid taskId
} + + ); +} diff --git a/pages/custom/jobs/index.tsx b/pages/custom/jobs/index.tsx new file mode 100644 index 0000000..8671ed8 --- /dev/null +++ b/pages/custom/jobs/index.tsx @@ -0,0 +1,198 @@ +import React, { useContext, useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { StringParam, useQueryParam, withDefault } from 'use-query-params'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPageHeader, + EuiSpacer, +} from '@elastic/eui'; +import { + ACTIVE_TASKS_LIST_TABLE_LOCAL_STORAGE_KEY, + COMPLETED_TASKS_LIST_TABLE_LOCAL_STORAGE_KEY, + DEFAULT_PAGE_SIZE, + StoredTableConfig, + WfoFilterTabs, + WfoIsAllowedToRender, + WfoStartTaskButtonComboBox, + WfoTableColumns, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { PATH_TASKS } from '@orchestrator-ui/orchestrator-ui-components'; +import { + ProcessListItem, + WfoProcessesList, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { PolicyResource } from '@orchestrator-ui/orchestrator-ui-components'; +import { ConfirmationDialogContext } from '@orchestrator-ui/orchestrator-ui-components'; +import { + useCheckEngineStatus, + useDataDisplayParams, + useMutateProcess, + useOrchestratorTheme, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { WfoRefresh } from '@orchestrator-ui/orchestrator-ui-components'; +import { + WfoTasksListTabType, + defaultTasksListTabs, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { getTasksListTabTypeFromString } from '@orchestrator-ui/orchestrator-ui-components'; +import { SortOrder } from '@orchestrator-ui/orchestrator-ui-components'; + +export default function JobsListPage() { + const router = useRouter(); + const t = useTranslations('jobsList'); + const [activeTab, setActiveTab] = useQueryParam( + 'activeTab', + withDefault(StringParam, WfoTasksListTabType.ACTIVE), + ); + + const [tableDefaults, setTableDefaults] = + useState>(); + + const selectedTasksListTab = getTasksListTabTypeFromString(activeTab); + + const localStorageKey = + selectedTasksListTab === WfoTasksListTabType.ACTIVE + ? ACTIVE_TASKS_LIST_TABLE_LOCAL_STORAGE_KEY + : COMPLETED_TASKS_LIST_TABLE_LOCAL_STORAGE_KEY; + + const getStoredTableConfig = + useStoredTableConfig(localStorageKey); + + const { theme } = useOrchestratorTheme(); + const { showConfirmDialog } = useContext(ConfirmationDialogContext); + const { retryAllProcesses } = useMutateProcess(); + const { isEngineRunningNow } = useCheckEngineStatus(); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: 'lastModifiedAt', + order: SortOrder.DESC, + }, + }); + + const handleChangeTasksListTab = ( + updatedTasksListTab: WfoTasksListTabType, + ) => { + setActiveTab(updatedTasksListTab); + setDataDisplayParam('pageIndex', 0); + }; + + const alwaysOnFilters = defaultTasksListTabs.find( + ({ id }) => id === selectedTasksListTab, + )?.alwaysOnFilters; + + if (!selectedTasksListTab) { + router.replace(PATH_TASKS); + return null; + } + + const handleRerunAllButtonClick = async () => { + if (await isEngineRunningNow()) { + showConfirmDialog({ + question: t('rerunAllQuestion'), + confirmAction: () => { + retryAllProcesses.mutate(); + }, + }); + } + }; + + // Changing the order of the keys, resulting in an updated column order in the table + const handleOverrideTableColumns: ( + defaultTableColumns: WfoTableColumns, + ) => WfoTableColumns = (defaultTableColumns) => ({ + workflowName: { + field: 'workflowName', + name: t('jobName'), + render: (value, { processId }) => ( + {value} + ), + }, + lastStep: defaultTableColumns.lastStep, + lastStatus: defaultTableColumns.lastStatus, + workflowTarget: defaultTableColumns.workflowTarget, + productTag: defaultTableColumns.tag, + productName: defaultTableColumns.productName, + customer: defaultTableColumns.customer, + customerAbbreviation: defaultTableColumns.customerAbbreviation, + subscriptions: defaultTableColumns.subscriptions, + createdBy: defaultTableColumns.createdBy, + assignee: defaultTableColumns.assignee, + processId: defaultTableColumns.processId, + startedAt: defaultTableColumns.startedAt, + lastModifiedAt: defaultTableColumns.lastModifiedAt, + }); + + return ( + <> + + + + + + + + + + {' '} + + ( + + )} + > + {t('rerunAll')} + + + + + + + + + + + + + + + ); +} diff --git a/pages/custom/jobs/new/[jobName].tsx b/pages/custom/jobs/new/[jobName].tsx new file mode 100644 index 0000000..efd033b --- /dev/null +++ b/pages/custom/jobs/new/[jobName].tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { WfoStartProcessPage } from '@orchestrator-ui/orchestrator-ui-components'; + +export default function StartJobsPage() { + const router = useRouter(); + const { jobName } = router.query; + + if (jobName && typeof jobName === 'string') { + return ; + } + + return
Invalid arguments provided
; +} diff --git a/pages/custom/notifications/[notificationId].tsx b/pages/custom/notifications/[notificationId].tsx new file mode 100644 index 0000000..51368a9 --- /dev/null +++ b/pages/custom/notifications/[notificationId].tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { + TreeProvider, + WfoSubscription, +} from '@orchestrator-ui/orchestrator-ui-components'; + +export default function WfoNotificationDetailPage() { + const router = useRouter(); + const { notificationId } = router.query; + + return ( + (notificationId && ( + + + + )) || <> + ); +} diff --git a/pages/custom/notifications/index.tsx b/pages/custom/notifications/index.tsx new file mode 100644 index 0000000..e991534 --- /dev/null +++ b/pages/custom/notifications/index.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; +import { StringParam, useQueryParam, withDefault } from 'use-query-params'; + +import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import type { + StoredTableConfig, + SubscriptionListItem, +} from '@orchestrator-ui/orchestrator-ui-components'; +import { + DEFAULT_PAGE_SIZE, + SUBSCRIPTIONS_TABLE_LOCAL_STORAGE_KEY, + SortOrder, + WfoFilterTabs, + WfoSubscriptionListTab, + WfoSubscriptionsList, + subscriptionListTabs, + useDataDisplayParams, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; + +export default function NotificationsPage() { + const t = useTranslations('notifications'); + + const [tableDefaults, setTableDefaults] = + useState>(); + + const getStoredTableConfig = useStoredTableConfig( + SUBSCRIPTIONS_TABLE_LOCAL_STORAGE_KEY, + ); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // the fist time before the useEffect has populated the tableDefaults. Better is to create a way for + // the query to wait for the values to be available + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: 'startDate', + order: SortOrder.DESC, + }, + }); + + const [activeTab, setActiveTab] = useQueryParam( + 'activeTab', + withDefault(StringParam, WfoSubscriptionListTab.ACTIVE), + ); + + const selectedTab = ((): WfoSubscriptionListTab => { + return ( + subscriptionListTabs.find(({ id }) => id === activeTab)?.id || + WfoSubscriptionListTab.ACTIVE + ); + })(); + + const handleChangeSubscriptionsTab = ( + updatedSubscriptionsTab: WfoSubscriptionListTab, + ) => { + setActiveTab(updatedSubscriptionsTab); + setDataDisplayParam('pageIndex', 0); + }; + + const alwaysOnFilters = subscriptionListTabs.find( + ({ id }) => id === activeTab, + )?.alwaysOnFilters; + + return ( + <> + + + + + + + + + + + ); +} diff --git a/pages/custom/routines/[routineId].tsx b/pages/custom/routines/[routineId].tsx new file mode 100644 index 0000000..2c4538a --- /dev/null +++ b/pages/custom/routines/[routineId].tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { WfoProcessDetailPage } from '@orchestrator-ui/orchestrator-ui-components'; + +export default function RoutineDetailPage() { + const router = useRouter(); + const { routineId } = router.query; + + return ( + <> + {(routineId && typeof routineId === 'string' && ( + + )) ||
Invalid workflowId
} + + ); +} diff --git a/pages/custom/routines/index.tsx b/pages/custom/routines/index.tsx new file mode 100644 index 0000000..4264583 --- /dev/null +++ b/pages/custom/routines/index.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; + +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/router'; +import { StringParam, useQueryParam, withDefault } from 'use-query-params'; + +import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { + ACTIVE_PROCESSES_LIST_TABLE_LOCAL_STORAGE_KEY, + COMPLETED_PROCESSES_LIST_TABLE_LOCAL_STORAGE_KEY, + DEFAULT_PAGE_SIZE, + SortOrder, + WfoFilterTabs, + WfoProcessesList, + WfoWorkflowsListTabType, + defaultWorkflowsListTabs, + getWorkflowsListTabTypeFromString, + useDataDisplayParams, + useStoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; +import type { + ProcessListItem, + StoredTableConfig, +} from '@orchestrator-ui/orchestrator-ui-components'; + +export default function RoutineListPage() { + const router = useRouter(); + const t = useTranslations('routines'); + const [activeTab, setActiveTab] = useQueryParam( + 'activeTab', + withDefault(StringParam, WfoWorkflowsListTabType.ACTIVE), + ); + + const [tableDefaults, setTableDefaults] = + useState>(); + + const selectedWorkflowsListTab = + getWorkflowsListTabTypeFromString(activeTab); + + const localStorageKey = + selectedWorkflowsListTab === WfoWorkflowsListTabType.ACTIVE + ? ACTIVE_PROCESSES_LIST_TABLE_LOCAL_STORAGE_KEY + : COMPLETED_PROCESSES_LIST_TABLE_LOCAL_STORAGE_KEY; + + const getStoredTableConfig = + useStoredTableConfig(localStorageKey); + + useEffect(() => { + const storedConfig = getStoredTableConfig(); + + if (storedConfig) { + setTableDefaults(storedConfig); + } + }, [getStoredTableConfig]); + + const { dataDisplayParams, setDataDisplayParam } = + useDataDisplayParams({ + // TODO: Improvement: A default pageSize value is set to avoid a graphql error when the query is executed + // https://github.com/workfloworchestrator/orchestrator-ui/issues/261 + pageSize: tableDefaults?.selectedPageSize || DEFAULT_PAGE_SIZE, + sortBy: { + field: 'lastModifiedAt', + order: SortOrder.DESC, + }, + }); + + const handleChangeWorkflowsListTab = ( + updatedWorkflowsListTab: WfoWorkflowsListTabType, + ) => { + setActiveTab(updatedWorkflowsListTab); + setDataDisplayParam('pageIndex', 0); + }; + + const alwaysOnFilters = defaultWorkflowsListTabs.find( + ({ id }) => id === selectedWorkflowsListTab, + )?.alwaysOnFilters; + + if (!selectedWorkflowsListTab) { + router.replace('/custom/routines'); + return null; + } + + return ( + <> + + + + + + + + + + + ); +} diff --git a/pages/custom/routines/new/[routineName].tsx b/pages/custom/routines/new/[routineName].tsx new file mode 100644 index 0000000..f3070e8 --- /dev/null +++ b/pages/custom/routines/new/[routineName].tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { useRouter } from 'next/router'; + +import { WfoStartProcessPage } from '@orchestrator-ui/orchestrator-ui-components'; + +export default function StartRoutinePage() { + const router = useRouter(); + const { routineName } = router.query; + + if (routineName && typeof routineName === 'string') { + return ; + } + + return
Invalid arguments provided
; +} diff --git a/translations/en-GB.json b/translations/en-GB.json index 0967ef4..d5e24b7 100644 --- a/translations/en-GB.json +++ b/translations/en-GB.json @@ -1 +1,70 @@ -{} +{ + "notifications": { + "title": "Notifications", + + "tabs": { + "active": "Active", + "terminated": "Tab 2", + "transient": "Tab 3", + "all": "All" + }, + + "loadingStatus": "Loading status" + }, + "jobsList": { + "rerunAll": "Rerun all jobs", + "rerunAllQuestion": "Sure?", + "jobName": "Job name" + }, + "routines": { + "title": "Routines", + "tabs": { + "active": "Started", + "completed": "Stopped" + } + }, + "configurations": { + "fixedVersionTypes": { + "resourceId": "Fixed version ID", + "type": "Fixed version type", + "description": "Description", + "usedInProductBlocks": "Used where" + }, + "productBlocks": { + "id": "ID", + "name": "Name", + "tag": "Tag", + "description": "Description", + "status": "Status", + "dependingProductBlocks": "Depends on", + "resourceTypes": "Fixed inputs" + }, + "products": { + "id": "ID", + "name": "Name", + "tag": "Tag", + "description": "Description", + "productType": "Product type", + "status": "Status", + "fixedInputs": "Fixed inputs", + "productBlocks": "Product blocks", + "createdAt": "Created at" + }, + "routines": { + "name": "Routine", + "description": "Description", + "target": "Goal", + "createdAt": "Created at", + "productTags": "Product tags" + } + }, + "metadata": { + "title": "Configurations", + "tabs": { + "fixedVersionTypes": "Fixed version types", + "products": "Products", + "productBlocks": "Product blocks", + "routines": "Routines" + } + } +}