diff --git a/.eslintignore b/.eslintignore index 9bce0a4dd4..75f0ff3a09 100755 --- a/.eslintignore +++ b/.eslintignore @@ -4,6 +4,7 @@ *.test.tsx .eslintrc.js vite.config.mts +scripts/ # The following files have eslint errors/warnings src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx @@ -101,10 +102,7 @@ src/components/app/details/testViewer/TestRunDetails.tsx src/components/app/details/testViewer/TestRunList.tsx src/components/app/details/triggerView/EmptyStateCIMaterial.tsx src/components/app/details/triggerView/MaterialSource.tsx -src/components/app/details/triggerView/TriggerView.tsx src/components/app/details/triggerView/__tests__/triggerview.test.tsx -src/components/app/details/triggerView/cdMaterial.tsx -src/components/app/details/triggerView/ciMaterial.tsx src/components/app/details/triggerView/ciWebhook.service.ts src/components/app/details/triggerView/config.ts src/components/app/details/triggerView/workflow.service.ts diff --git a/.gitignore b/.gitignore index 4fa9ac069c..c9a30bdb3a 100755 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ yalc.lock !.yarn/sdks !.yarn/versions .pnp.* + +.env.secrets +scripts/ \ No newline at end of file diff --git a/index.css b/index.css index 2d20935f0c..41d4cb846b 100644 --- a/index.css +++ b/index.css @@ -2,6 +2,7 @@ @import url('https://fonts.googleapis.com/css2?family=Inconsolata&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300..700&display=swap'); /* * Although this is duplicated but this would help us with consistent loader in case diff --git a/package.json b/package.json index e6bb2c168a..00dd85ec73 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.19.0", + "@devtron-labs/devtron-fe-common-lib": "1.19.0-pre-6", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", @@ -19,7 +19,6 @@ "dayjs": "^1.11.8", "dompurify": "^3.2.4", "fast-json-patch": "^3.1.1", - "flexsearch": "^0.6.32", "jsonpath-plus": "^10.3.0", "moment": "^2.29.4", "query-string": "^7.1.1", @@ -49,6 +48,7 @@ "lint": "tsc --noEmit && eslint 'src/**/*.{js,jsx,ts,tsx}' --max-warnings 0", "lint-fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", "start": "vite --open", + "dev": "node scripts/secrets-mgr.js", "build": "NODE_OPTIONS=--max_old_space_size=8192 vite build", "serve": "vite preview", "build-light": "NODE_OPTIONS=--max_old_space_size=8192 GENERATE_SOURCEMAP=false vite build", diff --git a/src/Pages/ChartStore/ChartDetails/ChartDetailsAbout.tsx b/src/Pages/ChartStore/ChartDetails/ChartDetailsAbout.tsx index 14895bd3ec..bf4805cf45 100644 --- a/src/Pages/ChartStore/ChartDetails/ChartDetailsAbout.tsx +++ b/src/Pages/ChartStore/ChartDetails/ChartDetailsAbout.tsx @@ -134,16 +134,18 @@ export const ChartDetailsAbout = ({ chartDetails, isLoading }: ChartDetailsAbout return (
- } - /> +
+ } + /> +

{name}

{description}

diff --git a/src/Pages/Shared/CommandBar/CommandBar.component.tsx b/src/Pages/Shared/CommandBar/CommandBar.component.tsx new file mode 100644 index 0000000000..8269905ac8 --- /dev/null +++ b/src/Pages/Shared/CommandBar/CommandBar.component.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' + +import { useRegisterShortcut, UseRegisterShortcutProvider } from '@devtron-labs/devtron-fe-common-lib' + +import CommandBarBackdrop from './CommandBarBackdrop' +import { SHORT_CUTS } from './constants' + +import './CommandBar.scss' + +const CommandBar = () => { + const [showCommandBar, setShowCommandBar] = useState(false) + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + + const handleOpen = () => { + setShowCommandBar(true) + } + + const handleClose = () => { + setShowCommandBar(false) + } + + useEffect(() => { + const { keys } = SHORT_CUTS.OPEN_COMMAND_BAR + + registerShortcut({ + keys, + description: SHORT_CUTS.OPEN_COMMAND_BAR.description, + callback: handleOpen, + }) + + return () => { + unregisterShortcut(keys) + } + }, []) + + if (!showCommandBar) { + return null + } + + return ( + + + + ) +} + +export default CommandBar diff --git a/src/Pages/Shared/CommandBar/CommandBar.scss b/src/Pages/Shared/CommandBar/CommandBar.scss new file mode 100644 index 0000000000..9f404c195c --- /dev/null +++ b/src/Pages/Shared/CommandBar/CommandBar.scss @@ -0,0 +1,19 @@ +.command-bar { + &__container { + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.04), 0 2px 8px 0 rgba(0, 0, 0, 0.04), 0 3px 17px 0 rgba(0, 0, 0, 0.04), 0 4px 30px 0 rgba(0, 0, 0, 0.13), 0 8px 48px 0 rgba(0, 0, 0, 0.15); + + &--selected-item { + background: var(--bg-hover); + } + + .search-bar { + &:focus-within { + border: none !important; + } + + &:hover { + background: transparent !important; + } + } + } +} \ No newline at end of file diff --git a/src/Pages/Shared/CommandBar/CommandBarBackdrop.tsx b/src/Pages/Shared/CommandBar/CommandBarBackdrop.tsx new file mode 100644 index 0000000000..b894c18832 --- /dev/null +++ b/src/Pages/Shared/CommandBar/CommandBarBackdrop.tsx @@ -0,0 +1,346 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { useHistory } from 'react-router-dom' + +import { + API_STATUS_CODES, + Backdrop, + GenericFilterEmptyState, + getUserPreferences, + KeyboardShortcut, + logExceptionToSentry, + ResponseType, + SearchBar, + stopPropagation, + SupportedKeyboardKeysType, + ToastManager, + ToastVariantType, + updateUserPreferences, + useQuery, + useRegisterShortcut, + UserPreferencesType, +} from '@devtron-labs/devtron-fe-common-lib' + +import CommandGroup from './CommandGroup' +import { NAVIGATION_GROUPS, RECENT_ACTIONS_GROUP, RECENT_NAVIGATION_ITEM_ID_PREFIX, SHORT_CUTS } from './constants' +import { CommandBarBackdropProps, CommandBarGroupType } from './types' +import { getNewSelectedIndex, sanitizeItemId } from './utils' + +const CommandBarBackdrop = ({ handleClose }: CommandBarBackdropProps) => { + const history = useHistory() + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + + const [searchText, setSearchText] = useState('') + const [selectedItemIndex, setSelectedItemIndex] = useState(0) + + const { data: recentActionsGroup, isLoading } = useQuery({ + queryFn: ({ signal }) => + getUserPreferences(signal).then((response) => { + const responseData: ResponseType = { + code: API_STATUS_CODES.OK, + status: 'OK', + result: response, + } + return responseData + }), + queryKey: ['recentNavigationActions'], + select: ({ result }) => + result.commandBar.recentNavigationActions.reduce((acc, action) => { + const requiredGroup = structuredClone(NAVIGATION_GROUPS).find((group) => + group.items.some((item) => item.id === action.id), + ) + + if (requiredGroup) { + const requiredItem = requiredGroup.items.find((item) => item.id === action.id) + requiredItem.id = `${RECENT_NAVIGATION_ITEM_ID_PREFIX}${action.id}` + acc.items.push(structuredClone(requiredItem)) + } + return acc + }, structuredClone(RECENT_ACTIONS_GROUP)), + }) + + const areFiltersApplied = !!searchText + + const searchBarRef = useRef(null) + const itemRefMap = useRef>({}) + + const handleSearchChange = (value: string) => { + setSearchText(value) + if (value !== searchText) { + setSelectedItemIndex(0) + } + } + + const updateItemRefMap = (id: string, el: HTMLDivElement) => { + itemRefMap.current[id] = el + } + + const filteredGroups = useMemo(() => { + const lowerCaseSearchText = searchText.toLowerCase() + + if (!searchText) { + return NAVIGATION_GROUPS + } + + return NAVIGATION_GROUPS.reduce((acc, group) => { + const filteredItems = group.items.filter((item) => item.title.toLowerCase().includes(lowerCaseSearchText)) + + if (filteredItems.length > 0) { + acc.push({ + ...group, + items: filteredItems, + }) + } + + return acc + }, []) + }, [searchText]) + + const itemFlatList: CommandBarGroupType['items'] = useMemo(() => { + if (areFiltersApplied) { + return filteredGroups.flatMap((group) => group.items) + } + + return recentActionsGroup + ? [...recentActionsGroup.items, ...NAVIGATION_GROUPS.flatMap((group) => group.items)] + : [...NAVIGATION_GROUPS.flatMap((group) => group.items)] + }, [areFiltersApplied, recentActionsGroup, filteredGroups]) + + const handleClearFilters = () => { + setSearchText('') + } + + const handleEscape = () => { + if (searchText) { + handleClearFilters() + return + } + + handleClose() + } + + const focusSearchBar = () => { + if (searchBarRef.current) { + searchBarRef.current.focus() + } + } + + const handleNavigation = (type: 'up' | 'down') => { + if (!itemFlatList.length) { + return + } + + setSelectedItemIndex((prevIndex) => { + const newIndex = getNewSelectedIndex(prevIndex, type, itemFlatList.length) + const item = itemFlatList[newIndex] + const itemElement = itemRefMap.current[item.id] + if (itemElement) { + itemElement.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }) + } + return newIndex + }) + } + + const onItemClick = async (item: CommandBarGroupType['items'][number]) => { + if (!item.href) { + logExceptionToSentry(new Error(`CommandBar item with id ${item.id} does not have a valid href`)) + ToastManager.showToast({ + variant: ToastVariantType.error, + description: `CommandBar item with id ${item.id} does not have a valid href`, + }) + return + } + + history.push(item.href) + handleClose() + + const currentItemId = sanitizeItemId(item) + + // In this now we will put the id as first item in the list and keep first 5 items then + const updatedRecentActions: UserPreferencesType['commandBar']['recentNavigationActions'] = [ + { + id: currentItemId, + }, + ...(recentActionsGroup?.items || []) + .filter((action) => sanitizeItemId(action) !== currentItemId) + .slice(0, 4) + .map((action) => ({ + id: sanitizeItemId(action), + })), + ] + + await updateUserPreferences({ + path: 'commandBar.recentNavigationActions', + value: updatedRecentActions, + }) + } + + const handleEnterSelectedItem = async () => { + const selectedItem = itemFlatList[selectedItemIndex] + + if (selectedItem) { + await onItemClick(selectedItem) + } + } + + // Intention: To retain the selected item index when recent actions are loaded + useEffect(() => { + if (!isLoading && recentActionsGroup?.items?.length && !areFiltersApplied) { + if (selectedItemIndex !== 0) { + const selectedIndex = selectedItemIndex + recentActionsGroup.items.length + + const itemElement = itemRefMap.current[itemFlatList[selectedIndex]?.id] + if (itemElement) { + itemElement.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' }) + } + + setSelectedItemIndex(selectedIndex) + } + } + }, [isLoading, recentActionsGroup]) + + useEffect(() => { + const { keys, description } = SHORT_CUTS.FOCUS_SEARCH_BAR + + registerShortcut({ + keys, + description, + callback: focusSearchBar, + }) + + return () => { + unregisterShortcut(keys) + } + }, []) + + useEffect(() => { + const { keys, description } = SHORT_CUTS.ENTER_ITEM + + registerShortcut({ + keys, + description, + callback: handleEnterSelectedItem, + }) + + return () => { + unregisterShortcut(keys) + } + }, [selectedItemIndex, itemFlatList, recentActionsGroup]) + + useEffect(() => { + const navigateUpKeys = SHORT_CUTS.NAVIGATE_UP.keys + const navigateDownKeys = SHORT_CUTS.NAVIGATE_DOWN.keys + + registerShortcut({ + keys: navigateUpKeys, + description: SHORT_CUTS.NAVIGATE_UP.description, + callback: () => handleNavigation('up'), + }) + + registerShortcut({ + keys: navigateDownKeys, + description: SHORT_CUTS.NAVIGATE_DOWN.description, + callback: () => handleNavigation('down'), + }) + + return () => { + unregisterShortcut(navigateUpKeys) + unregisterShortcut(navigateDownKeys) + } + }, [itemFlatList]) + + const renderNavigationGroups = (baseIndex: number) => { + let nextIndex = baseIndex + + return filteredGroups.map((group) => { + nextIndex += group.items.length + return ( + + ) + }) + } + + // To handle native scroll behavior + const handleListBoxKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault() + } + } + + const renderKeyboardShortcuts = (keys: SupportedKeyboardKeysType[], label: string) => ( +
+ {keys.map((key) => ( + + ))} + {label} +
+ ) + + return ( + +
+
+
+ +
+ + {areFiltersApplied && !filteredGroups.length ? ( + + ) : ( +
+ {!areFiltersApplied && ( + + )} + + {renderNavigationGroups(areFiltersApplied ? 0 : recentActionsGroup?.items.length || 0)} +
+ )} +
+ +
+
+ {renderKeyboardShortcuts(['ArrowUp', 'ArrowDown'], 'to navigate')} + {renderKeyboardShortcuts(['Enter'], 'to select')} + {renderKeyboardShortcuts(['Escape'], 'to close')} +
+ {renderKeyboardShortcuts(['>'], 'to search actions')} +
+
+
+ ) +} + +export default CommandBarBackdrop diff --git a/src/Pages/Shared/CommandBar/CommandGroup.tsx b/src/Pages/Shared/CommandBar/CommandGroup.tsx new file mode 100644 index 0000000000..33056fafcb --- /dev/null +++ b/src/Pages/Shared/CommandBar/CommandGroup.tsx @@ -0,0 +1,71 @@ +import { Icon } from '@devtron-labs/devtron-fe-common-lib' + +import { CommandBarItemType, CommandGroupProps } from './types' + +const CommandGroup = ({ + title, + id, + items, + isLoading, + baseIndex, + selectedItemIndex, + updateItemRefMap, + onItemClick, +}: CommandGroupProps) => { + const updateItemRef = (elementId: string) => (el: HTMLDivElement) => { + if (el) { + updateItemRefMap(elementId, el) + } + } + + const getHandleItemClick = (item: CommandBarItemType) => () => { + onItemClick(item) + } + + const getIsItemSelected = (itemIndex: number) => selectedItemIndex === baseIndex + itemIndex + + const renderContent = () => { + if (isLoading || !items?.length) { + return ( +
+ {isLoading ? 'Loading...' : 'No items found'} +
+ ) + } + + return items.map((item, index) => ( +
+
+ +

{item.title}

+
+ + {getIsItemSelected(index) && } +
+ )) + } + + return ( +
+
+

+ {title} +

+
+ +
+ {renderContent()} +
+
+ ) +} + +export default CommandGroup diff --git a/src/Pages/Shared/CommandBar/constants.ts b/src/Pages/Shared/CommandBar/constants.ts new file mode 100644 index 0000000000..71aba4fe64 --- /dev/null +++ b/src/Pages/Shared/CommandBar/constants.ts @@ -0,0 +1,470 @@ +import { SupportedKeyboardKeysType } from '@devtron-labs/devtron-fe-common-lib' + +import { URLS } from '@Config/routes' + +import { CommandBarGroupType, NavigationGroupType } from './types' + +export const NAVIGATION_LIST: NavigationGroupType[] = [ + { + id: 'application-management', + title: 'Application Management', + icon: 'ic-grid-view', + items: [ + { + title: 'Overview', + dataTestId: 'application-management-overview', + id: 'application-management-overview', + icon: 'ic-speedometer', + }, + { + title: 'Applications', + dataTestId: 'click-on-application', + id: 'application-management-applications', + icon: 'ic-grid-view', + href: URLS.APP, + }, + { + title: 'Application Groups', + dataTestId: 'click-on-application-groups', + id: 'application-management-application-groups', + icon: 'ic-app-group', + href: URLS.APPLICATION_GROUP, + }, + { + title: 'Chart Store', + dataTestId: 'click-on-chart-store', + id: 'application-management-chart-store', + icon: 'ic-helm', + href: URLS.CHARTS, + }, + { + title: 'Bulk Edit', + dataTestId: 'click-on-bulk-edit', + id: 'application-management-bulk-edit', + icon: 'ic-code', + href: URLS.BULK_EDITS, + }, + { + title: 'Configurations', + dataTestId: 'click-on-configurations', + id: 'application-management-configurations', + hasSubMenu: true, + subItems: [ + { + title: 'GitOps', + dataTestId: 'click-on-configurations-gitops', + id: 'application-management-configurations-gitops', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Git accounts', + dataTestId: 'click-on-configurations-git-accounts', + id: 'application-management-configurations-git-accounts', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'External links', + dataTestId: 'click-on-configurations-external-links', + id: 'application-management-configurations-external-links', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Chart Repository', + dataTestId: 'click-on-configurations-chart-repository', + id: 'application-management-configurations-chart-repository', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Deployment Charts', + dataTestId: 'click-on-configurations-deployment-charts', + id: 'application-management-configurations-deployment-charts', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Notifications', + dataTestId: 'click-on-configurations-notifications', + id: 'application-management-configurations-notifications', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Catalog Frameworks', + dataTestId: 'click-on-configurations-catalog-frameworks', + id: 'application-management-configurations-catalog-frameworks', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Scoped Variables', + dataTestId: 'click-on-configurations-scoped-variables', + id: 'application-management-configurations-scoped-variables', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Build Infra', + dataTestId: 'click-on-configurations-build-infra', + id: 'application-management-configurations-build-infra', + href: URLS.GIT_OPS_CONFIG, + }, + ], + }, + { + title: 'Policies', + dataTestId: 'click-on-policies', + id: 'application-management-policies', + hasSubMenu: true, + subItems: [ + { + title: 'Deployment Window', + dataTestId: 'click-on-policies-deployment-window', + id: 'application-management-policies-deployment-window', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Approval policy', + dataTestId: 'click-on-policies-approval-policy', + id: 'application-management-policies-approval-policy', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Plugin policy', + dataTestId: 'click-on-policies-plugin-policy', + id: 'application-management-policies-plugin-policy', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Pull image digest', + dataTestId: 'click-on-policies-pull-image-digest', + id: 'application-management-policies-pull-image-digest', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Tag Policy', + dataTestId: 'click-on-policies-tag-policy', + id: 'application-management-policies-tag-policy', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Filter conditions', + dataTestId: 'click-on-policies-filter-conditions', + id: 'application-management-policies-filter-conditions', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Lock Deployment configuration', + dataTestId: 'click-on-policies-lock-deployment-configuration', + id: 'application-management-policies-lock-deployment-configuration', + href: URLS.GIT_OPS_CONFIG, + }, + ], + }, + { + title: 'Others', + dataTestId: 'click-on-others', + id: 'application-management-others', + hasSubMenu: true, + subItems: [ + { + title: 'Application Templates', + dataTestId: 'click-on-others-application-templates', + id: 'application-management-others-application-templates', + href: URLS.GIT_OPS_CONFIG, + }, + { + title: 'Projects', + dataTestId: 'click-on-others-projects', + id: 'application-management-others-projects', + href: URLS.GIT_OPS_CONFIG, + }, + ], + }, + ], + }, + { + id: 'infrastructure-management', + title: 'Infrastructure Management', + icon: 'ic-grid-view', + items: [ + { + title: 'Overview', + dataTestId: 'infrastructure-management-overview', + id: 'infrastructure-management-overview', + icon: 'ic-speedometer', + }, + { + title: 'Resource Browser', + dataTestId: 'resource-browser', + id: 'infrastructure-management-resource-browser', + icon: 'ic-cube', + href: URLS.RESOURCE_BROWSER, + }, + { + title: 'Intercepted Changes', + dataTestId: 'intercepted-changes', + id: 'infrastructure-management-intercepted-changes', + icon: 'ic-file', + }, + { + title: 'Resource Watcher', + dataTestId: 'resource-watcher', + id: 'infrastructure-management-resource-watcher', + icon: 'ic-monitoring', + href: URLS.RESOURCE_WATCHER, + }, + { + title: 'Catalog Framework', + dataTestId: 'catalog-framework', + id: 'infrastructure-management-catalog-framework', + icon: 'ic-file', + }, + ], + }, + { + id: 'software-release-management', + title: 'Software Release Management', + icon: 'ic-open-box', + items: [ + { + title: 'Overview', + dataTestId: 'software-release-management-overview', + id: 'software-release-management-overview', + icon: 'ic-speedometer', + }, + { + title: 'Software Release', + dataTestId: 'software-release', + id: 'software-release-management-software-release', + icon: 'ic-open-box', + }, + ], + }, + { + id: 'cost-visibility', + title: 'Cost Visibility', + icon: 'ic-grid-view', + items: [ + { + title: 'Overview', + dataTestId: 'cost-visibility-overview', + id: 'cost-visibility-overview', + icon: 'ic-speedometer', + }, + { + title: 'Trends', + dataTestId: 'cost-visibility-trends', + id: 'cost-visibility-trends', + icon: 'ic-open-box', + }, + { + title: 'Cost Breakdown', + dataTestId: 'cost-breakdown', + id: 'cost-visibility-cost-breakdown', + hasSubMenu: true, + subItems: [ + { + title: 'Clusters', + dataTestId: 'cost-breakdown-clusters', + id: 'cost-visibility-cost-breakdown-clusters', + }, + { + title: 'Environments', + dataTestId: 'cost-breakdown-environments', + id: 'cost-visibility-cost-breakdown-environments', + }, + { + title: 'Projects', + dataTestId: 'cost-breakdown-projects', + id: 'cost-visibility-cost-breakdown-projects', + }, + { + title: 'Applications', + dataTestId: 'cost-breakdown-applications', + id: 'cost-visibility-cost-breakdown-applications', + }, + ], + }, + { + title: 'Configurations', + dataTestId: 'cost-visibility-configurations', + id: 'cost-visibility-configurations', + icon: 'ic-gear', + }, + ], + }, + { + id: 'security-center', + title: 'Security Center', + icon: 'ic-shield-check', + items: [ + { + title: 'Overview', + dataTestId: 'security-center-overview', + id: 'security-center-overview', + icon: 'ic-speedometer', + }, + { + title: 'Application Security', + dataTestId: 'application-security', + id: 'security-center-application-security', + icon: 'ic-bug', + }, + { + title: 'Security Policies', + dataTestId: 'security-policies', + id: 'security-center-security-policies', + icon: 'ic-gavel', + }, + ], + }, + { + id: 'automation-and-enablement', + title: 'Automation & Enablement', + icon: 'ic-grid-view', + items: [ + { + title: 'Jobs', + dataTestId: 'jobs', + id: 'automation-and-enablement-jobs', + icon: 'ic-k8s-job', + }, + { + title: 'Alerting', + dataTestId: 'alerting', + id: 'automation-and-enablement-alerting', + icon: 'ic-bug', + }, + { + title: 'Incident Response', + dataTestId: 'incident-response', + id: 'automation-and-enablement-incident-response', + icon: 'ic-bug', + }, + { + title: 'API portal', + dataTestId: 'api-portal', + id: 'automation-and-enablement-api-portal', + icon: 'ic-code', + }, + { + title: 'Runbook Automation', + dataTestId: 'runbook-automation', + id: 'automation-and-enablement-runbook-automation', + icon: 'ic-book-open', + }, + ], + }, + { + id: 'global-configuration', + title: 'Global Configuration', + icon: 'ic-gear', + items: [ + { + title: 'SSO Login Services', + dataTestId: 'sso-login-services', + id: 'global-configuration-sso-login-services', + icon: 'ic-key', + }, + { + title: 'Host URLS', + dataTestId: 'host-urls', + id: 'global-configuration-host-urls', + icon: 'ic-link', + }, + { + title: 'Cluster & environments', + dataTestId: 'cluster-and-environments', + id: 'global-configuration-cluster-and-environments', + icon: 'ic-cluster', + }, + { + title: 'Container/OCI Registry', + dataTestId: 'container-oci-registry', + id: 'global-configuration-container-oci-registry', + icon: 'ic-folder', + }, + { + title: 'Authorization', + dataTestId: 'authorization', + id: 'global-configuration-authorization', + hasSubMenu: true, + subItems: [ + { + title: 'User Permissions', + dataTestId: 'user-permissions', + id: 'global-configuration-authorization-user-permissions', + }, + { + title: 'Permission Groups', + dataTestId: 'permission-groups', + id: 'global-configuration-authorization-permission-groups', + }, + { + title: 'API Tokens', + dataTestId: 'authorization-api-tokens', + id: 'global-configuration-authorization-api-tokens', + }, + ], + }, + ], + }, +] + +export const NAVIGATION_GROUPS: CommandBarGroupType[] = NAVIGATION_LIST.map((group) => ({ + title: group.title, + id: group.id, + items: group.items.flatMap(({ hasSubMenu, subItems, title, href, id, icon }) => { + if (hasSubMenu && subItems?.length) { + return subItems.map((subItem) => ({ + title: `${title} / ${subItem.title}`, + id: subItem.id, + // Since icon is not present for some subItems, using from group + icon: group.icon, + // TODO: No href present for some subItems + href: subItem.href ?? null, + })) + } + + return { + title, + id, + icon: icon || 'ic-arrow-right', + // TODO: No href present for some items + href: href ?? null, + } + }), +})) + +export const RECENT_ACTIONS_GROUP: CommandBarGroupType = { + id: 'command-bar-recent-navigation-group', + items: [], + title: 'Recent Navigation', +} + +export const RECENT_NAVIGATION_ITEM_ID_PREFIX = 'recent-navigation-' as const + +export const SHORT_CUTS: Record< + 'OPEN_COMMAND_BAR' | 'FOCUS_SEARCH_BAR' | 'NAVIGATE_UP' | 'NAVIGATE_DOWN' | 'ENTER_ITEM', + { + keys: SupportedKeyboardKeysType[] + description: string + } +> = { + OPEN_COMMAND_BAR: { + keys: ['Meta', 'K'], + description: 'Open Command Bar', + }, + FOCUS_SEARCH_BAR: { + keys: ['Shift', '>'], + description: 'Focus Search Bar', + }, + NAVIGATE_UP: { + keys: ['ArrowUp'], + description: 'Navigate Up', + }, + NAVIGATE_DOWN: { + keys: ['ArrowDown'], + description: 'Navigate Down', + }, + ENTER_ITEM: { + keys: ['Enter'], + description: 'Select Item', + }, +} diff --git a/src/Pages/Shared/CommandBar/index.ts b/src/Pages/Shared/CommandBar/index.ts new file mode 100644 index 0000000000..f3edbf70a7 --- /dev/null +++ b/src/Pages/Shared/CommandBar/index.ts @@ -0,0 +1 @@ +export { default as CommandBar } from './CommandBar.component' diff --git a/src/Pages/Shared/CommandBar/types.ts b/src/Pages/Shared/CommandBar/types.ts new file mode 100644 index 0000000000..ffff43f4cf --- /dev/null +++ b/src/Pages/Shared/CommandBar/types.ts @@ -0,0 +1,94 @@ +import { + customEnv, + IconsProps, + NavigationItemID, + NavigationSubMenuItemID, + Never, + URLS as CommonURLS, + UserPreferencesType, +} from '@devtron-labs/devtron-fe-common-lib' + +import { URLS } from '@Config/routes' + +import { RECENT_NAVIGATION_ITEM_ID_PREFIX } from './constants' + +export type NavigationRootItemID = + | 'application-management' + | 'infrastructure-management' + | 'software-release-management' + | 'cost-visibility' + | 'security-center' + | 'automation-and-enablement' + | 'global-configuration' + +type CommonNavigationItemType = { + title: string + dataTestId: string + icon: IconsProps['name'] + href?: (typeof URLS)[keyof typeof URLS] | (typeof CommonURLS)[keyof typeof CommonURLS] +} + +export type NavigationItemType = Pick & { + isAvailableInEA?: boolean + markOnlyForSuperAdmin?: boolean + forceHideEnvKey?: keyof customEnv + title: string + hideNav?: boolean + markAsBeta?: boolean + isAvailableInDesktop?: boolean + moduleName?: string + moduleNameTrivy?: string + id: NavigationItemID +} & ( + | (Pick & { + hasSubMenu?: false + subItems?: never + }) + | (Never> & { + hasSubMenu: true + subItems: (Omit & { id: NavigationSubMenuItemID })[] + }) + ) + +export interface NavigationGroupType extends Pick { + id: NavigationRootItemID + items: NavigationItemType[] +} + +export type CommandBarActionIdType = UserPreferencesType['commandBar']['recentNavigationActions'][number]['id'] + +export type CommandBarItemType = { + id: CommandBarActionIdType | `${typeof RECENT_NAVIGATION_ITEM_ID_PREFIX}${CommandBarActionIdType}` + title: string + icon: IconsProps['name'] +} & ( + | { + href: CommonNavigationItemType['href'] + onSelect?: never + } + | { + href?: never + onSelect: (e: React.MouseEvent) => void + } +) + +export interface CommandBarGroupType { + /** + * Required for semantic purpose, and need to be unique across all groups. + */ + id: string + title: string + items: CommandBarItemType[] +} + +export interface CommandGroupProps extends CommandBarGroupType { + isLoading?: boolean + baseIndex: number + selectedItemIndex: number + updateItemRefMap: (id: string, el: HTMLDivElement) => void + onItemClick: (item: CommandBarItemType) => void +} + +export interface CommandBarBackdropProps { + handleClose: () => void +} diff --git a/src/Pages/Shared/CommandBar/utils.ts b/src/Pages/Shared/CommandBar/utils.ts new file mode 100644 index 0000000000..a786b2339e --- /dev/null +++ b/src/Pages/Shared/CommandBar/utils.ts @@ -0,0 +1,14 @@ +import { RECENT_NAVIGATION_ITEM_ID_PREFIX } from './constants' +import { CommandBarActionIdType, CommandBarItemType } from './types' + +export const sanitizeItemId = (item: CommandBarItemType) => + (item.id.startsWith(RECENT_NAVIGATION_ITEM_ID_PREFIX) + ? item.id.replace(RECENT_NAVIGATION_ITEM_ID_PREFIX, '') + : item.id) as CommandBarActionIdType + +export const getNewSelectedIndex = (prevIndex: number, type: 'up' | 'down', totalItems: number) => { + if (type === 'up') { + return prevIndex === 0 ? totalItems - 1 : prevIndex - 1 + } + return prevIndex === totalItems - 1 ? 0 : prevIndex + 1 +} diff --git a/src/components/ApplicationGroup/AppGroup.types.ts b/src/components/ApplicationGroup/AppGroup.types.ts index 24b89053ae..7788a53a51 100644 --- a/src/components/ApplicationGroup/AppGroup.types.ts +++ b/src/components/ApplicationGroup/AppGroup.types.ts @@ -14,31 +14,27 @@ * limitations under the License. */ -import { Dispatch, SetStateAction } from 'react' import { MultiValue } from 'react-select' import { ACTION_STATE, AppInfoListType, - ApprovalConfigDataType, - CDModalTabType, + CIMaterialType, CommonNodeAttr, DeploymentNodeType, - DeploymentStrategyTypeWithDefault, - FilterConditionsListType, GVKType, MODAL_TYPE, OptionType, - PipelineIdsVsDeploymentStrategyMap, ResponseType, RuntimePluginVariables, + ServerErrors, + TriggerBlockedInfo, UseUrlFiltersReturnType, - WorkflowNodeType, WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' -import { TIME_STAMP_ORDER } from '@Components/app/details/triggerView/Constants' -import { CDMaterialProps, RuntimeParamsErrorState, WebhookPayloadType } from '@Components/app/details/triggerView/types' +import { GitInfoMaterialProps } from '@Components/app/details/triggerView/BuildImageModal/types' +import { DeployImageContentProps } from '@Components/app/details/triggerView/DeployImageModal/types' import { AppConfigState, EnvConfigurationsNavProps, @@ -50,7 +46,6 @@ import { WorkloadCheckType } from '../v2/appDetails/sourceInfo/scaleWorkloads/sc import { AppFilterTabs, BulkResponseStatus } from './Constants' interface BulkTriggerAppDetailType { - workFlowId: string appId: number name: string material?: any[] @@ -58,50 +53,53 @@ interface BulkTriggerAppDetailType { } export interface BulkCIDetailType extends BulkTriggerAppDetailType { - ciPipelineName: string - ciPipelineId: string - isFirstTrigger: boolean - isCacheAvailable: boolean - isLinkedCI: boolean - isLinkedCD: boolean - title: string - isJobCI: boolean - isWebhookCI: boolean - parentAppId: number - parentCIPipelineId: number + workflowId: string + material: CIMaterialType[] + runtimeParams: RuntimePluginVariables[] + node: CommonNodeAttr errorMessage: string - hideSearchHeader: boolean + runtimeParamsInitialError: ServerErrors | null + materialInitialError: ServerErrors | null filteredCIPipelines: any + ciConfiguredGitMaterialId: WorkflowType['ciConfiguredGitMaterialId'] + runtimeParamsErrorState: GitInfoMaterialProps['runtimeParamsErrorState'] + ignoreCache: boolean +} + +export type BulkCDDetailDerivedFromNode = Required< + Pick< + DeployImageContentProps, + | 'pipelineId' + | 'appId' + | 'parentEnvironmentName' + | 'isTriggerBlockedDueToPlugin' + | 'configurePluginURL' + | 'triggerType' + | 'appName' + > +> & { + stageNotAvailable: boolean + errorMessage: string + triggerBlockedInfo: TriggerBlockedInfo + consequence: CommonNodeAttr['pluginBlockState'] + showPluginWarning: CommonNodeAttr['showPluginWarning'] } +export type BulkCDDetailType = BulkCDDetailDerivedFromNode & + Pick & { + /** + * True in cases when we reload materials on single app + */ + areMaterialsLoading: boolean + materialError: ServerErrors | null + tagsWarningMessage: string + } + export interface BulkCDDetailTypeResponse { bulkCDDetailType: BulkCDDetailType[] uniqueReleaseTags: string[] } -export interface BulkCDDetailType - extends BulkTriggerAppDetailType, - Pick, - Partial> { - cdPipelineName?: string - cdPipelineId?: string - stageType?: DeploymentNodeType - triggerType?: string - envName: string - envId: number - parentPipelineId?: string - parentPipelineType?: WorkflowNodeType - parentEnvironmentName?: string - approvalConfigData?: ApprovalConfigDataType - requestedUserId?: number - appReleaseTags?: string[] - tagsEditable?: boolean - ciPipelineId?: number - hideImageTaggingHardDelete?: boolean - resourceFilters?: FilterConditionsListType[] - isExceptionUser?: boolean -} - export type TriggerVirtualEnvResponseRowType = | { isVirtual: true @@ -123,61 +121,6 @@ export type ResponseRowType = { envId?: number } & TriggerVirtualEnvResponseRowType -interface BulkRuntimeParamsType { - runtimeParams: Record - setRuntimeParams: React.Dispatch>> - runtimeParamsErrorState: Record - setRuntimeParamsErrorState: React.Dispatch>> -} - -export interface BulkCITriggerType extends BulkRuntimeParamsType { - appList: BulkCIDetailType[] - closePopup: (e) => void - updateBulkInputMaterial: (materialList: Record) => void - onClickTriggerBulkCI: (appIgnoreCache: Record, appsToRetry?: Record) => void - getWebhookPayload: (id, webhookTimeStampOrder?: typeof TIME_STAMP_ORDER) => void - webhookPayloads: WebhookPayloadType - setWebhookPayloads: React.Dispatch> - isWebhookPayloadLoading: boolean - isShowRegexModal: (_appId: number, ciNodeId: number, inputMaterialList: any[]) => boolean - responseList: ResponseRowType[] - isLoading: boolean - setLoading: React.Dispatch> - setPageViewType: React.Dispatch> -} - -export interface BulkCDTriggerType extends BulkRuntimeParamsType { - stage: DeploymentNodeType - appList: BulkCDDetailType[] - closePopup: (e) => void - updateBulkInputMaterial: (materialList: Record) => void - onClickTriggerBulkCD: ( - skipIfHibernated: boolean, - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap, - appsToRetry?: Record, - ) => void - changeTab?: ( - materrialId: string | number, - artifactId: number, - tab: CDModalTabType, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - ) => void - toggleSourceInfo?: (materialIndex: number, selectedCDDetail?: { id: number; type: DeploymentNodeType }) => void - selectImage?: ( - index: number, - materialType: string, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - ) => void - responseList: ResponseRowType[] - isLoading: boolean - setLoading: React.Dispatch> - isVirtualEnv?: boolean - uniqueReleaseTags: string[] - feasiblePipelineIds: Set - bulkDeploymentStrategy: DeploymentStrategyTypeWithDefault - setBulkDeploymentStrategy: Dispatch> -} - export interface ProcessWorkFlowStatusType { cicdInProgress: boolean workflows: WorkflowType[] @@ -211,15 +154,11 @@ export interface TriggerResponseModalBodyProps { type RetryFailedType = | { - onClickRetryDeploy: BulkCDTriggerType['onClickTriggerBulkCD'] - skipHibernatedApps: boolean - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap + onClickRetryDeploy: (appsToRetry: Record) => void onClickRetryBuild?: never } | { onClickRetryDeploy?: never - skipHibernatedApps?: never - pipelineIdVsStrategyMap?: never onClickRetryBuild: (appsToRetry: Record) => void } @@ -233,11 +172,6 @@ export interface TriggerModalRowType { isVirtualEnv?: boolean } -export interface WorkflowNodeSelectionType { - id: number - name: string - type: WorkflowNodeType -} export interface WorkflowAppSelectionType { id: number name: string diff --git a/src/components/ApplicationGroup/AppGroup.utils.ts b/src/components/ApplicationGroup/AppGroup.utils.ts index 0443df9fd0..3e20762308 100644 --- a/src/components/ApplicationGroup/AppGroup.utils.ts +++ b/src/components/ApplicationGroup/AppGroup.utils.ts @@ -96,28 +96,37 @@ export const processWorkflowStatuses = ( } }) } - // Update Workflow using maps - const _workflows = workflowsList.map((wf) => { - wf.nodes = wf.nodes.map((node) => { + // Update Workflow using maps, returning new objects for reactivity + const _workflows = workflowsList.map((wf) => ({ + ...wf, + nodes: wf.nodes.map((node) => { switch (node.type) { case 'CI': - node.status = ciMap[node.id]?.status - node.storageConfigured = ciMap[node.id]?.storageConfigured - break + return { + ...node, + status: ciMap[node.id]?.status, + storageConfigured: ciMap[node.id]?.storageConfigured, + } case 'PRECD': - node.status = preCDMap[node.id] - break + return { + ...node, + status: preCDMap[node.id], + } case 'POSTCD': - node.status = postCDMap[node.id] - break + return { + ...node, + status: postCDMap[node.id], + } case 'CD': - node.status = cdMap[node.id] - break + return { + ...node, + status: cdMap[node.id], + } + default: + return { ...node } } - return node }) - return wf - }) + })) return { cicdInProgress, workflows: _workflows } } diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx deleted file mode 100644 index f24733bb8e..0000000000 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx +++ /dev/null @@ -1,1024 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { SyntheticEvent, useEffect, useRef, useState } from 'react' -import { useHistory, useLocation } from 'react-router-dom' - -import { - ACTION_STATE, - AnimatedDeployButton, - ApiQueuingWithBatch, - Button, - ButtonStyleType, - ButtonVariantType, - CD_MATERIAL_SIDEBAR_TABS, - CDMaterialResponseType, - CDMaterialServiceEnum, - CDMaterialSidebarType, - CDMaterialType, - CommonNodeAttr, - ComponentSizeType, - DEPLOYMENT_WINDOW_TYPE, - DeploymentNodeType, - DeploymentWindowProfileMetaData, - Drawer, - FilterStates, - genericCDMaterialsService, - GenericEmptyState, - Icon, - ImageComment, - MODAL_TYPE, - PipelineIdsVsDeploymentStrategyMap, - ReleaseTag, - RuntimePluginVariables, - SelectPicker, - showError, - stopPropagation, - ToastManager, - ToastVariantType, - TriggerBlockType, - uploadCDPipelineFile, - UploadFileProps, - useGetUserRoles, - useMainContext, -} from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as UnAuthorized } from '@Icons/ic-locked.svg' -import { ReactComponent as Tag } from '@Icons/ic-tag.svg' -import { ReactComponent as Error } from '@Icons/ic-warning.svg' -import { getIsMaterialApproved } from '@Components/app/details/triggerView/cdMaterials.utils' - -import emptyPreDeploy from '../../../../assets/img/empty-pre-deploy.webp' -import { ReactComponent as MechanicalOperation } from '../../../../assets/img/ic-mechanical-operation.svg' -import notAuthorized from '../../../../assets/img/ic-not-authorized.svg' -import CDMaterial from '../../../app/details/triggerView/cdMaterial' -import { BulkSelectionEvents, MATERIAL_TYPE, RuntimeParamsErrorState } from '../../../app/details/triggerView/types' -import { importComponentFromFELibrary } from '../../../common' -import { BulkCDDetailType, BulkCDTriggerType } from '../../AppGroup.types' -import { BULK_CD_DEPLOYMENT_STATUS, BULK_CD_MATERIAL_STATUS, BULK_CD_MESSAGING, BUTTON_TITLE } from '../../Constants' -import { BULK_ERROR_MESSAGES } from './constants' -import TriggerResponseModalBody, { TriggerResponseModalFooter } from './TriggerResponseModal' -import { - getIsImageApprovedByDeployerSelected, - getIsNonApprovedImageSelected, - getSelectedAppListForBulkStrategy, -} from './utils' - -const DeploymentWindowInfoBar = importComponentFromFELibrary('DeploymentWindowInfoBar') -const BulkDeployResistanceTippy = importComponentFromFELibrary('BulkDeployResistanceTippy') -const processDeploymentWindowMetadata = importComponentFromFELibrary( - 'processDeploymentWindowMetadata', - null, - 'function', -) -const getDeploymentWindowStateAppGroup = importComponentFromFELibrary( - 'getDeploymentWindowStateAppGroup', - null, - 'function', -) -const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const MissingPluginBlockState = importComponentFromFELibrary('MissingPluginBlockState', null, 'function') -const PolicyEnforcementMessage = importComponentFromFELibrary('PolicyEnforcementMessage') -const TriggerBlockedError = importComponentFromFELibrary('TriggerBlockedError', null, 'function') -const TriggerBlockEmptyState = importComponentFromFELibrary('TriggerBlockEmptyState', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) -const SkipHibernatedCheckbox = importComponentFromFELibrary('SkipHibernatedCheckbox', null, 'function') -const SelectDeploymentStrategy = importComponentFromFELibrary('SelectDeploymentStrategy', null, 'function') -const BulkCDStrategy = importComponentFromFELibrary('BulkCDStrategy', null, 'function') - -// TODO: Fix release tags selection -const BulkCDTrigger = ({ - stage, - appList, - closePopup, - // NOTE: Using this to update the appList in the parent component, should remove this later - updateBulkInputMaterial, - // NOTE: Should trigger the bulk cd here only but since its also calling another parent function not refactoring right now - onClickTriggerBulkCD, - feasiblePipelineIds, - responseList, - isLoading, - setLoading, - isVirtualEnv, - uniqueReleaseTags, - runtimeParams, - setRuntimeParams, - bulkDeploymentStrategy, - setBulkDeploymentStrategy, - runtimeParamsErrorState, - setRuntimeParamsErrorState, -}: BulkCDTriggerType) => { - const { canFetchHelmAppStatus } = useMainContext() - const [selectedApp, setSelectedApp] = useState( - appList.find((app) => !app.warningMessage) || appList[0], - ) - const [tagNotFoundWarningsMap, setTagNotFoundWarningsMap] = useState>(new Map()) - const [unauthorizedAppList, setUnauthorizedAppList] = useState>({}) - const abortControllerRef = useRef(new AbortController()) - const [appSearchTextMap, setAppSearchTextMap] = useState>({}) - const [selectedImages, setSelectedImages] = useState>({}) - // This signifies any action that needs to be propagated to the child - const [selectedImageFromBulk, setSelectedImageFromBulk] = useState(null) - const [appDeploymentWindowMap, setAppDeploymentWindowMap] = useState< - Record - >({}) - const [isPartialActionAllowed, setIsPartialActionAllowed] = useState(false) - const [showResistanceBox, setShowResistanceBox] = useState(false) - const [currentSidebarTab, setCurrentSidebarTab] = useState(CDMaterialSidebarType.IMAGE) - const [skipHibernatedApps, setSkipHibernatedApps] = useState(false) - const [showStrategyFeasibilityPage, setShowStrategyFeasibilityPage] = useState(false) - const [pipelineIdVsStrategyMap, setPipelineIdVsStrategyMap] = useState({}) - - const location = useLocation() - const history = useHistory() - const { isSuperAdmin } = useGetUserRoles() - const isBulkDeploymentTriggered = useRef(false) - - const showRuntimeParams = - RuntimeParamTabs && (stage === DeploymentNodeType.PRECD || stage === DeploymentNodeType.POSTCD) - - useEffect(() => { - const searchParams = new URLSearchParams(location.search) - const search = searchParams.get('search') - const _appSearchTextMap = { ...appSearchTextMap } - - if (search) { - _appSearchTextMap[selectedApp.appId] = search - } else { - delete _appSearchTextMap[selectedApp.appId] - } - - setAppSearchTextMap(_appSearchTextMap) - }, [location]) - - const closeBulkCDModal = (e: React.MouseEvent): void => { - e.stopPropagation() - abortControllerRef.current.abort() - closePopup(e) - } - - const [selectedTagName, setSelectedTagName] = useState<{ label: string; value: string }>({ - label: 'latest', - value: 'latest', - }) - - const handleSidebarTabChange = (e: React.ChangeEvent) => { - setCurrentSidebarTab(e.target.value as CDMaterialSidebarType) - } - - const handleRuntimeParamError = (errorState: RuntimeParamsErrorState) => { - setRuntimeParamsErrorState((prevErrorState) => ({ - ...prevErrorState, - [selectedApp.appId]: errorState, - })) - } - - const handleRuntimeParamChange = (currentAppRuntimeParams: RuntimePluginVariables[]) => { - const clonedRuntimeParams = structuredClone(runtimeParams) - clonedRuntimeParams[selectedApp.appId] = currentAppRuntimeParams - setRuntimeParams(clonedRuntimeParams) - } - - const bulkUploadFile = ({ file, allowedExtensions, maxUploadSize }: UploadFileProps) => - uploadCDPipelineFile({ - file, - allowedExtensions, - maxUploadSize, - appId: selectedApp.appId, - envId: selectedApp.envId, - }) - - const getDeploymentWindowData = async (_cdMaterialResponse) => { - const currentEnv = appList[0].envId - const appEnvMap = [] - let _isPartialActionAllowed = false - for (const appDetails of appList) { - if (_cdMaterialResponse[appDetails.appId]) { - appEnvMap.push({ appId: appDetails.appId, envId: appDetails.envId }) - } - } - const { result } = await getDeploymentWindowStateAppGroup(appEnvMap) - const _appDeploymentWindowMap = {} - result?.appData?.forEach((data) => { - _appDeploymentWindowMap[data.appId] = processDeploymentWindowMetadata( - data.deploymentProfileList, - currentEnv, - ) - if (!_isPartialActionAllowed) { - _isPartialActionAllowed = - _appDeploymentWindowMap[data.appId].type === DEPLOYMENT_WINDOW_TYPE.BLACKOUT || - !_appDeploymentWindowMap[data.appId].isActive - ? _appDeploymentWindowMap[data.appId].userActionState === ACTION_STATE.PARTIAL - : false - } - }) - setIsPartialActionAllowed(_isPartialActionAllowed) - setAppDeploymentWindowMap(_appDeploymentWindowMap) - } - - const resolveMaterialData = (_cdMaterialResponse, _unauthorizedAppList) => (response) => { - if (response.status === 'fulfilled') { - setRuntimeParams((prevState) => { - const updatedRuntimeParams = { ...prevState } - updatedRuntimeParams[response.value.appId] = response.value.runtimeParams || [] - return updatedRuntimeParams - }) - - _cdMaterialResponse[response.value.appId] = response.value - // if first image does not have filerState.ALLOWED then unselect all images and set SELECT_NONE for selectedImage and for first app send the trigger of SELECT_NONE from selectedImageFromBulk - if ( - response.value.materials?.length > 0 && - (response.value.materials[0].filterState !== FilterStates.ALLOWED || - response.value.materials[0].vulnerable) - ) { - const updatedMaterials = response.value.materials.map((mat) => ({ - ...mat, - isSelected: false, - })) - _cdMaterialResponse[response.value.appId] = { - ...response.value, - materials: updatedMaterials, - } - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [response.value.appId]: BulkSelectionEvents.SELECT_NONE, - })) - - const _warningMessage = response.value.materials[0].vulnerable - ? 'has security vulnerabilities' - : 'is not eligible' - - setTagNotFoundWarningsMap((prevTagNotFoundWarningsMap) => { - const _tagNotFoundWarningsMap = new Map(prevTagNotFoundWarningsMap) - _tagNotFoundWarningsMap.set( - response.value.appId, - `Tag '${ - selectedTagName.value?.length > 15 - ? `${selectedTagName.value.substring(0, 10)}...` - : selectedTagName.value - }' ${_warningMessage}`, - ) - return _tagNotFoundWarningsMap - }) - if (response.value.appId === selectedApp.appId) { - setSelectedImageFromBulk(BulkSelectionEvents.SELECT_NONE) - } - } else if (response.value.materials?.length === 0) { - setTagNotFoundWarningsMap((prevTagNotFoundWarningsMap) => { - const _tagNotFoundWarningsMap = new Map(prevTagNotFoundWarningsMap) - _tagNotFoundWarningsMap.set( - response.value.appId, - `Tag '${ - selectedTagName.value?.length > 15 - ? `${selectedTagName.value.substring(0, 10)}...` - : selectedTagName.value - }' not found`, - ) - return _tagNotFoundWarningsMap - }) - } - - delete _unauthorizedAppList[response.value.appId] - } else { - const errorReason = response?.reason - if (errorReason?.code === 403) { - _unauthorizedAppList[errorReason.appId] = true - } - } - } - - const getCDMaterialFunction = (appDetails) => () => - // Not sending any query params since its not necessary on mount and filters and handled by other service) - genericCDMaterialsService( - CDMaterialServiceEnum.CD_MATERIALS, - Number(appDetails.cdPipelineId), - appDetails.stageType, - abortControllerRef.current.signal, - { - offset: 0, - size: 20, - }, - ) - .then((data) => ({ appId: appDetails.appId, ...data })) - .catch((e) => { - if (!abortControllerRef.current.signal.aborted) { - throw { response: e?.response, appId: appDetails.appId } - } - }) - - /** - * Gets triggered during the mount state of the component through useEffect - * Fetches the material data pushes them into promise list - * Promise list is resolved using Promise.allSettled - * If the promise is fulfilled, the data is pushed into cdMaterialResponse - */ - const getMaterialData = (): void => { - abortControllerRef.current = new AbortController() - const _unauthorizedAppList: Record = {} - const _cdMaterialResponse: Record = {} - abortControllerRef.current = new AbortController() - const _cdMaterialFunctionsList = [] - for (const appDetails of appList) { - if (!appDetails.warningMessage) { - _unauthorizedAppList[appDetails.appId] = false - _cdMaterialFunctionsList.push(getCDMaterialFunction(appDetails)) - } - } - - if (!_cdMaterialFunctionsList.length) { - setLoading(false) - return - } - - ApiQueuingWithBatch(_cdMaterialFunctionsList) - .then(async (responses: any[]) => { - responses.forEach(resolveMaterialData(_cdMaterialResponse, _unauthorizedAppList)) - if (getDeploymentWindowStateAppGroup) { - await getDeploymentWindowData(_cdMaterialResponse) - } - updateBulkInputMaterial(_cdMaterialResponse) - setUnauthorizedAppList(_unauthorizedAppList) - setLoading(false) - }) - .catch((error) => { - setLoading(false) - showError(error) - }) - } - - useEffect(() => { - getMaterialData() - }, []) - - const handleBackFromStrategySelection = () => { - setShowStrategyFeasibilityPage(false) - setPipelineIdVsStrategyMap({}) - } - - const renderHeaderSection = (): JSX.Element => ( -
-
- {showStrategyFeasibilityPage && ( -
-
- ) - - const changeApp = (e): void => { - const updatedErrorState = validateRuntimeParameters(runtimeParams[selectedApp.appId]) - handleRuntimeParamError(updatedErrorState) - if (!updatedErrorState.isValid) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: BULK_ERROR_MESSAGES.CHANGE_APPLICATION, - }) - return - } - - const _selectedApp = appList[e.currentTarget.dataset.index] - setSelectedApp(_selectedApp) - setSelectedImageFromBulk(selectedImages[_selectedApp.appId]) - - if (appSearchTextMap[_selectedApp.appId]) { - const newSearchParams = new URLSearchParams(location.search) - newSearchParams.set('search', appSearchTextMap[_selectedApp.appId]) - - history.push({ - search: newSearchParams.toString(), - }) - } else { - history.push({ - search: '', - }) - } - } - - const renderEmptyView = (): JSX.Element => { - if (selectedApp.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG) { - return - } - - if (selectedApp.isTriggerBlockedDueToPlugin) { - const commonNodeAttrType: CommonNodeAttr['type'] = - selectedApp.stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD' - - return ( - - ) - } - - if (unauthorizedAppList[selectedApp.appId]) { - return ( - - ) - } - return ( - - ) - } - - const renderDeploymentWithoutApprovalWarning = (app: BulkCDDetailType) => { - if (!app.isExceptionUser) { - return null - } - - const selectedMaterial: CDMaterialType = app.material?.find((mat: CDMaterialType) => mat.isSelected) - - if (!selectedMaterial || getIsMaterialApproved(selectedMaterial?.userApprovalMetadata)) { - return null - } - - return ( -
- -

Non-approved image selected

-
- ) - } - - const renderAppWarningAndErrors = (app: BulkCDDetailType) => { - const commonNodeAttrType: CommonNodeAttr['type'] = - app.stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD' - - const warningMessage = app.warningMessage || appDeploymentWindowMap[app.appId]?.warningMessage - - const isAppSelected = selectedApp.appId === app.appId - - if (unauthorizedAppList[app.appId]) { - return ( -
- - {BULK_CD_MESSAGING.unauthorized.title} -
- ) - } - - if (tagNotFoundWarningsMap.has(app.appId)) { - return ( -
- - - {tagNotFoundWarningsMap.get(app.appId)} -
- ) - } - - if (app.isTriggerBlockedDueToPlugin) { - return ( - - ) - } - - if (app.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG) { - return - } - - if (!!warningMessage && !app.showPluginWarning) { - return ( -
- - {warningMessage} -
- ) - } - - if (app.showPluginWarning) { - return ( - - ) - } - - return null - } - - const responseListLength = responseList.length - - const renderBodySection = (): JSX.Element => { - if (responseListLength) { - return ( - - ) - } - - if (isLoading) { - const message = isBulkDeploymentTriggered.current - ? BULK_CD_DEPLOYMENT_STATUS(appList.length, appList[0].envName) - : BULK_CD_MATERIAL_STATUS(appList.length) - return ( - - ) - } - - const updateCurrentAppMaterial = (matId: number, releaseTags?: ReleaseTag[], imageComment?: ImageComment) => { - const updatedCurrentApp = selectedApp - updatedCurrentApp?.material.forEach((mat) => { - if (mat.id === matId) { - if (releaseTags) { - mat.imageReleaseTags = releaseTags - } - if (imageComment) { - mat.imageComment = imageComment - } - } - }) - updatedCurrentApp && setSelectedApp(updatedCurrentApp) - } - - const _currentApp = appList.find((app) => app.appId === selectedApp.appId) ?? ({} as BulkCDDetailType) - uniqueReleaseTags.sort((a, b) => a.localeCompare(b)) - - const tagsList = ['latest', 'active'] - - tagsList.push(...uniqueReleaseTags) - const options = tagsList.map((tag) => ({ label: tag, value: tag })) - - const appWiseTagsToArtifactIdMapMappings = {} - appList.forEach((app) => { - if (!app.material?.length) { - appWiseTagsToArtifactIdMapMappings[app.appId] = {} - } else { - const tagsToArtifactIdMap = { latest: 0 } - for (let i = 0; i < app.material?.length; i++) { - const mat = app.material?.[i] - mat.imageReleaseTags?.forEach((imageTag) => { - tagsToArtifactIdMap[imageTag.tagName] = i - }) - - if (mat.deployed && mat.latest) { - tagsToArtifactIdMap['active'] = i - } - } - appWiseTagsToArtifactIdMapMappings[app.appId] = tagsToArtifactIdMap - } - }) - - // Don't use it as single, use it through update function - const selectImageLocal = (index: number, appId: number, selectedImageTag: string) => { - setSelectedImages({ ...selectedImages, [appId]: selectedImageTag }) - - if (appWiseTagsToArtifactIdMapMappings[appId][selectedTagName.value] !== index) { - const _tagNotFoundWarningsMap = tagNotFoundWarningsMap - _tagNotFoundWarningsMap.delete(appId) - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - setSelectedTagName({ value: 'Multiple tags', label: 'Multiple tags' }) - } else { - // remove warning if any - const _tagNotFoundWarningsMap = new Map(tagNotFoundWarningsMap) - _tagNotFoundWarningsMap.delete(appId) - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - } - } - - const parseApplistIntoCDMaterialResponse = ( - appListData: BulkCDDetailType, - updatedMaterials?: CDMaterialType, - ) => ({ - materials: updatedMaterials ?? appListData.material, - requestedUserId: appListData.requestedUserId, - approvalConfigData: appListData.approvalConfigData, - appReleaseTagNames: appListData.appReleaseTags, - tagsEditable: appListData.tagsEditable, - }) - - const handleTagChange = (selectedTag) => { - setSelectedTagName(selectedTag) - const _tagNotFoundWarningsMap = new Map() - const _cdMaterialResponse: Record = {} - - for (let i = 0; i < (appList?.length ?? 0); i++) { - const app = appList[i] - const tagsToArtifactIdMap = appWiseTagsToArtifactIdMapMappings[app.appId] - let artifactIndex = -1 - if (typeof tagsToArtifactIdMap[selectedTag.value] !== 'undefined') { - artifactIndex = tagsToArtifactIdMap[selectedTag.value] - } - - // Handling the behavior for excluded filter state - if (artifactIndex !== -1) { - const selectedImageFilterState = app.material?.[artifactIndex]?.filterState - - if (selectedImageFilterState !== FilterStates.ALLOWED) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' is not eligible`, - ) - } else if (app.material?.[artifactIndex]?.vulnerable) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' has security vulnerabilities`, - ) - } - } else { - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' not found`, - ) - } - - if (artifactIndex !== -1 && selectedTag.value !== 'latest' && selectedTag.value !== 'active') { - const releaseTag = app.material[artifactIndex]?.imageReleaseTags.find( - (releaseTag) => releaseTag.tagName === selectedTag.value, - ) - if (releaseTag?.deleted) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' is soft-deleted`, - ) - } - } - - if (artifactIndex !== -1) { - const selectedImageName = app.material?.[artifactIndex]?.image - const updatedMaterials: any = app.material?.map((mat, index) => ({ - ...mat, - isSelected: index === artifactIndex, - })) - - _cdMaterialResponse[app.appId] = parseApplistIntoCDMaterialResponse(app, updatedMaterials) - - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [app.appId]: selectedImageName, - })) - } else { - const updatedMaterials: any = app.material?.map((mat) => ({ - ...mat, - isSelected: false, - })) - - _cdMaterialResponse[app.appId] = parseApplistIntoCDMaterialResponse(app, updatedMaterials) - - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [app.appId]: BulkSelectionEvents.SELECT_NONE, - })) - } - } - - updateBulkInputMaterial(_cdMaterialResponse) - - // Handling to behviour of current app to send a trigger to child - const selectedImageName = _cdMaterialResponse[selectedApp.appId]?.materials?.find( - (mat: CDMaterialType) => mat.isSelected === true, - )?.image - - if (selectedImageName) { - setSelectedImageFromBulk(selectedImageName) - } else { - setSelectedImageFromBulk(BulkSelectionEvents.SELECT_NONE) - } - - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - } - - const updateBulkCDMaterialsItem = (singleCDMaterialResponse) => { - const _cdMaterialResponse: Record = {} - _cdMaterialResponse[selectedApp.appId] = singleCDMaterialResponse - - updateBulkInputMaterial(_cdMaterialResponse) - - const selectedArtifact = singleCDMaterialResponse?.materials?.find( - (mat: CDMaterialType) => mat.isSelected === true, - ) - - if (selectedArtifact) { - selectImageLocal(selectedArtifact.index, selectedApp.appId, selectedArtifact.image) - } - - // Setting it to null since since only wants to trigger change inside if user changes app or tag - setSelectedImageFromBulk(null) - } - - return ( -
-
-
- {showRuntimeParams && ( -
- -
- )} - {currentSidebarTab === CDMaterialSidebarType.IMAGE && ( - <> - Select image by release tag -
- } - onChange={handleTagChange} - isDisabled={false} - classNamePrefix="build-config__select-repository-containing-code" - autoFocus - /> -
- - )} -
- APPLICATIONS -
-
- {appList.map((app, index) => ( -
- {app.name} - {renderDeploymentWithoutApprovalWarning(app)} - {renderAppWarningAndErrors(app)} -
- ))} -
-
- {selectedApp.warningMessage || unauthorizedAppList[selectedApp.appId] ? ( - renderEmptyView() - ) : ( - // TODO: Handle isSuperAdmin prop - - <> - {DeploymentWindowInfoBar && - appDeploymentWindowMap[selectedApp.appId] && - appDeploymentWindowMap[selectedApp.appId].warningMessage && ( - - )} - - - )} -
-
- ) - } - const hideResistanceBox = (): void => { - setShowResistanceBox(false) - } - - const onClickDeploy = (e: SyntheticEvent) => { - if (showStrategyFeasibilityPage) { - setShowStrategyFeasibilityPage(false) - } - if (isPartialActionAllowed && BulkDeployResistanceTippy && !showResistanceBox) { - setShowResistanceBox(true) - } else { - isBulkDeploymentTriggered.current = true - stopPropagation(e) - onClickTriggerBulkCD(skipHibernatedApps, pipelineIdVsStrategyMap) - setShowResistanceBox(false) - } - } - - const onClickStartDeploy = (e): void => { - if (BulkCDStrategy && bulkDeploymentStrategy !== 'DEFAULT') { - setShowStrategyFeasibilityPage(true) - return - } - onClickDeploy(e) - } - - const isDeployDisabled = (): boolean => - appList.every((app) => app.warningMessage || tagNotFoundWarningsMap.has(app.appId) || !app.material?.length) - - const renderFooterSection = (): JSX.Element => { - if (responseListLength) { - return ( - - ) - } - - const isCDStage = stage === DeploymentNodeType.CD - const isDeployButtonDisabled: boolean = isDeployDisabled() - const canDeployWithoutApproval = getIsNonApprovedImageSelected(appList) - const canImageApproverDeploy = getIsImageApprovedByDeployerSelected(appList) - const showSkipHibernatedCheckbox = !!SkipHibernatedCheckbox && canFetchHelmAppStatus - - return ( -
- {showSkipHibernatedCheckbox && ( - app.appId)} - skipHibernated={skipHibernatedApps} - setSkipHibernated={setSkipHibernatedApps} - /> - )} -
- {isCDStage && SelectDeploymentStrategy && !isLoading && !responseListLength && ( - +app.cdPipelineId)} - isBulkStrategyChange - deploymentStrategy={bulkDeploymentStrategy} - setDeploymentStrategy={setBulkDeploymentStrategy} - /> - )} -
- } - onButtonClick={onClickStartDeploy} - disabled={isDeployButtonDisabled} - isLoading={isLoading} - animateStartIcon={isCDStage} - style={ - canDeployWithoutApproval || canImageApproverDeploy - ? ButtonStyleType.warning - : ButtonStyleType.default - } - tooltipContent={ - canDeployWithoutApproval || canImageApproverDeploy - ? 'You are authorized to deploy as an exception user for some applications' - : '' - } - /> -
-
-
- ) - } - - return ( - -
- {renderHeaderSection()} - {BulkCDStrategy && showStrategyFeasibilityPage ? ( - - ) : ( - <> - {renderBodySection()} - {renderFooterSection()} - - )} -
- {showResistanceBox && ( - - )} -
- ) -} - -export default BulkCDTrigger diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx deleted file mode 100644 index 0923e9371c..0000000000 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx +++ /dev/null @@ -1,835 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useContext, useEffect, useRef, useState } from 'react' -import { - ServerErrors, - Drawer, - showError, - stopPropagation, - ConsequenceType, - ConsequenceAction, - useAsync, - GenericEmptyState, - CIMaterialSidebarType, - ApiQueuingWithBatch, - ModuleNameMap, - SourceTypeMap, - ToastManager, - ToastVariantType, - CIMaterialType, - BlockedStateData, - PromiseAllStatusType, - CommonNodeAttr, - Button, - ButtonVariantType, - ComponentSizeType, - ButtonStyleType, - RuntimePluginVariables, - uploadCIPipelineFile, - UploadFileProps, - savePipeline, - DocLink, -} from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { getCIPipelineURL, getParsedBranchValuesForPlugin, importComponentFromFELibrary } from '../../../common' -import { ReactComponent as Close } from '../../../../assets/icons/ic-close.svg' -import { ReactComponent as PlayIcon } from '@Icons/ic-play-outline.svg' -import { ReactComponent as Warning } from '../../../../assets/icons/ic-warning.svg' -import { ReactComponent as ICError } from '../../../../assets/icons/ic-alert-triangle.svg' -import { ReactComponent as Storage } from '../../../../assets/icons/ic-storage.svg' -import { ReactComponent as InfoIcon } from '../../../../assets/icons/info-filled.svg' -import externalCiImg from '../../../../assets/img/external-ci.webp' -import linkedCDBuildCIImg from '../../../../assets/img/linked-cd-bulk-ci.webp' -import linkedCiImg from '../../../../assets/img/linked-ci.webp' -import { getModuleConfigured } from '../../../app/details/appDetails/appDetails.service' -import { SOURCE_NOT_CONFIGURED, URLS, ViewType } from '../../../../config' -import MaterialSource from '../../../app/details/triggerView/MaterialSource' -import { TriggerViewContext } from '../../../app/details/triggerView/config' -import { getCIMaterialList } from '../../../app/service' -import { - HandleRuntimeParamChange, - HandleRuntimeParamErrorState, - RegexValueType, -} from '../../../app/details/triggerView/types' -import { EmptyView } from '../../../app/details/cicdHistory/History.components' -import BranchRegexModal from '../../../app/details/triggerView/BranchRegexModal' -import { BulkCIDetailType, BulkCITriggerType } from '../../AppGroup.types' -import { IGNORE_CACHE_INFO } from '../../../app/details/triggerView/Constants' -import TriggerResponseModalBody, { TriggerResponseModalFooter } from './TriggerResponseModal' -import { BULK_CI_BUILD_STATUS, BULK_CI_MATERIAL_STATUS, BULK_CI_MESSAGING } from '../../Constants' -import { processConsequenceData } from '../../AppGroup.utils' -import { getIsAppUnorthodox } from './utils' -import { ReactComponent as MechanicalOperation } from '../../../../assets/img/ic-mechanical-operation.svg' -import { BULK_ERROR_MESSAGES } from './constants' -import { GitInfoMaterial } from '@Components/common/helpers/GitInfoMaterialCard/GitInfoMaterial' -import { ReactComponent as LeftIcon } from '@Icons/ic-arrow-backward.svg' - -const PolicyEnforcementMessage = importComponentFromFELibrary('PolicyEnforcementMessage') -const getCIBlockState: (...props) => Promise = importComponentFromFELibrary( - 'getCIBlockState', - null, - 'function', -) -const getRuntimeParams = importComponentFromFELibrary('getRuntimeParams', null, 'function') -const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) - -const BulkCITrigger = ({ - appList, - closePopup, - updateBulkInputMaterial, - onClickTriggerBulkCI, - getWebhookPayload, - isShowRegexModal, - responseList, - isLoading, - setLoading, - runtimeParams, - setRuntimeParams, - runtimeParamsErrorState, - setRuntimeParamsErrorState, - setPageViewType, - webhookPayloads, - setWebhookPayloads, - isWebhookPayloadLoading, -}: BulkCITriggerType) => { - const [showRegexModal, setShowRegexModal] = useState(false) - const [isChangeBranchClicked, setChangeBranchClicked] = useState(false) - const [selectedApp, setSelectedApp] = useState(appList[0]) - - const [regexValue, setRegexValue] = useState>({}) - const [appIgnoreCache, setAppIgnoreCache] = useState>({}) - const [appPolicy, setAppPolicy] = useState>({}) - const [currentSidebarTab, setCurrentSidebarTab] = useState(CIMaterialSidebarType.CODE_SOURCE) - const [isWebhookBulkCI, setIsWebhookBulkCI] = useState(false) - - const selectedMaterialList = appList.find((app) => app.appId === selectedApp.appId)?.material || [] - - useEffect(() => { - const selectedMaterialId = selectedMaterialList[0]?.id - if (isWebhookBulkCI && selectedMaterialId) { - getWebhookPayload(selectedMaterialId) - } - }, [JSON.stringify(selectedMaterialList), isWebhookBulkCI]) - - const [blobStorageConfigurationLoading, blobStorageConfiguration] = useAsync( - () => getModuleConfigured(ModuleNameMap.BLOB_STORAGE), - [], - ) - const { - selectMaterial, - refreshMaterial, - }: { - selectMaterial: (materialId, pipelineId?: number) => void - refreshMaterial: (ciNodeId: number, materialId: number, abortController?: AbortController) => void - } = useContext(TriggerViewContext) - const abortControllerRef = useRef(new AbortController()) - const isBulkBuildTriggered = useRef(false) - - const closeBulkCIModal = (evt) => { - abortControllerRef.current.abort() - closePopup(evt) - } - - useEffect(() => { - for (const _app of appList) { - appIgnoreCache[_app.ciPipelineId] = false - } - getMaterialData() - }, []) - - const getInitSelectedRegexValue = (): Record => { - if (selectedApp.appId) { - const selectedMaterial = appList.find((app) => app.appId === selectedApp.appId).material - - if (selectedMaterial) { - return selectedMaterial.reduce( - (acc, mat) => { - acc[mat.gitMaterialId] = { - value: mat.value, - isInvalid: mat.regex ? !new RegExp(mat.regex).test(mat.value) : false, - } - return acc - }, - {} as Record, - ) - } - } - return {} - } - - const getRuntimeParamsData = async (_materialListMap: Record): Promise => { - const runtimeParamsServiceList = appList.map((appDetails) => { - if (getIsAppUnorthodox(appDetails) || !_materialListMap[appDetails.appId]) { - return () => [] - } - return () => getRuntimeParams(appDetails.ciPipelineId) - }) - - if (runtimeParamsServiceList.length) { - try { - // Appending any for legacy code, since we did not had generics in APIQueuingWithBatch - const responses: any[] = await ApiQueuingWithBatch(runtimeParamsServiceList, true) - const _runtimeParams: Record = {} - responses.forEach((res, index) => { - _runtimeParams[appList[index]?.ciPipelineId] = res.value || [] - }) - setRuntimeParams(_runtimeParams) - } catch (error) { - setPageViewType(ViewType.ERROR) - showError(error) - } - } - } - - const getMaterialData = (): void => { - abortControllerRef.current = new AbortController() - const _CIMaterialPromiseFunctionList = appList.map((appDetails) => - getIsAppUnorthodox(appDetails) - ? () => null - : () => - getCIMaterialList( - { - pipelineId: appDetails.ciPipelineId, - }, - abortControllerRef.current.signal, - ), - ) - if (_CIMaterialPromiseFunctionList?.length) { - const _materialListMap: Record = {} - // TODO: Remove then and use async await - ApiQueuingWithBatch(_CIMaterialPromiseFunctionList) - .then(async (responses: any[]) => { - responses.forEach((res, index) => { - _materialListMap[appList[index]?.appId] = res.value?.['result'] - }) - await getPolicyEnforcementData(_materialListMap) - if (getRuntimeParams) { - await getRuntimeParamsData(_materialListMap) - } - updateBulkInputMaterial(_materialListMap) - if (!getIsAppUnorthodox(selectedApp)) { - setShowRegexModal( - isShowRegexModal( - selectedApp.appId, - +selectedApp.ciPipelineId, - _materialListMap[selectedApp.appId], - ), - ) - } - setLoading(false) - }) - .catch((error) => { - if (!abortControllerRef.current.signal.aborted) { - showError(error) - setLoading(false) - } - }) - } else { - setLoading(false) - } - } - - const handleRuntimeParamError: HandleRuntimeParamErrorState = (errorState) => { - setRuntimeParamsErrorState((prevErrorState) => ({ - ...prevErrorState, - [selectedApp.ciPipelineId]: errorState, - })) - } - - const handleRuntimeParamChange: HandleRuntimeParamChange = (currentAppRuntimeParams) => { - const updatedRuntimeParams = structuredClone(runtimeParams) - updatedRuntimeParams[selectedApp.ciPipelineId] = currentAppRuntimeParams - setRuntimeParams(updatedRuntimeParams) - } - - const uploadFile = ({ file, allowedExtensions, maxUploadSize }: UploadFileProps) => - uploadCIPipelineFile({ - file, - allowedExtensions, - maxUploadSize, - appId: selectedApp.appId, - ciPipelineId: +selectedApp.ciPipelineId, - }) - - const handleSidebarTabChange = (e: React.ChangeEvent) => { - setCurrentSidebarTab(e.target.value as CIMaterialSidebarType) - } - - const getPolicyEnforcementData = async (_materialListMap: Record): Promise => { - if (!getCIBlockState) { - return null - } - - const policyPromiseFunctionList = appList.map((appDetails) => { - if (getIsAppUnorthodox(appDetails) || !_materialListMap[appDetails.appId]) { - return () => null - } - let branchNames = '' - for (const material of _materialListMap[appDetails.appId]) { - if ( - (!material.isBranchError && !material.isRepoError && !material.isRegex) || - material.value !== '--' - ) { - branchNames += `${branchNames ? ',' : ''}${getParsedBranchValuesForPlugin(material.value)}` - } - } - - return !branchNames - ? () => null - : () => getCIBlockState(appDetails.ciPipelineId, appDetails.appId, branchNames, appDetails.name) - }) - - if (policyPromiseFunctionList?.length) { - const policyListMap: Record = {} - try { - const responses = await ApiQueuingWithBatch(policyPromiseFunctionList, true) - responses.forEach((res, index) => { - if (res.status === PromiseAllStatusType.FULFILLED) { - policyListMap[appList[index]?.appId] = res.value ? processConsequenceData(res.value) : null - } - }) - setAppPolicy(policyListMap) - } catch (error) { - showError(error) - } - } - } - - const onCloseWebhookModal = () => { - setIsWebhookBulkCI(false) - setWebhookPayloads(null) - } - - const renderHeaderSection = (): JSX.Element | null => { - return ( -
-
- {isWebhookBulkCI && ( -
-
- ) - } - - const showBranchEditModal = (): void => { - setShowRegexModal(true) - setChangeBranchClicked(false) - setRegexValue(getInitSelectedRegexValue()) - } - - const hideBranchEditModal = (e?): void => { - if (e) { - stopPropagation(e) - } - setShowRegexModal(false) - setChangeBranchClicked(false) - } - - const changeApp = (e): void => { - const updatedErrorState = validateRuntimeParameters(runtimeParams[selectedApp.ciPipelineId]) - handleRuntimeParamError(updatedErrorState) - if (!updatedErrorState.isValid) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: BULK_ERROR_MESSAGES.CHANGE_APPLICATION, - }) - return - } - - stopPropagation(e) - const _selectedApp = appList[e.currentTarget.dataset.index] - if (_selectedApp.appId !== selectedApp.appId) { - setSelectedApp(_selectedApp) - if (getIsAppUnorthodox(_selectedApp)) { - setShowRegexModal(false) - } else { - setShowRegexModal( - isShowRegexModal(_selectedApp.appId, +_selectedApp.ciPipelineId, _selectedApp.material), - ) - } - } - } - - const saveBranchName = () => { - setLoading(true) - const payload: any = { - appId: selectedApp.appId, - id: +selectedApp.workFlowId, - ciPipelineMaterial: [], - } - - const selectedCIPipeline = selectedApp.filteredCIPipelines?.find( - (_ciPipeline) => _ciPipeline?.id == selectedApp.ciPipelineId, - ) - // Populate the ciPipelineMaterial with flatten object - if (selectedCIPipeline?.ciMaterial?.length) { - for (const _cm of selectedCIPipeline.ciMaterial) { - const regVal = regexValue[_cm.gitMaterialId] - let _updatedCM - if (regVal?.value && _cm.source.regex) { - isRegexValueInvalid(_cm) - - _updatedCM = { - ..._cm, - type: SourceTypeMap.BranchFixed, - value: regVal.value, - regex: _cm.source.regex, - } - } else { - // To maintain the flatten object structure supported by API for unchanged values - // as during update/next click it uses the fetched ciMaterial structure i.e. containing source - _updatedCM = { - ..._cm, - ..._cm.source, - } - } - - // Deleting as it's not required in the request payload - delete _updatedCM['source'] - payload.ciPipelineMaterial.push(_updatedCM) - } - } - - savePipeline(payload, { - isRegexMaterial: true, - isTemplateView: false, - }) - .then((response) => { - if (response) { - getMaterialData() - } else { - setLoading(true) - } - }) - .catch((error: ServerErrors) => { - showError(error) - setLoading(false) - }) - } - - const isRegexValueInvalid = (_cm): void => { - const regExp = new RegExp(_cm.source.regex) - const regVal = regexValue[_cm.gitMaterialId] - if (!regExp.test(regVal.value)) { - const _regexValue = { ...regexValue } - _regexValue[_cm.gitMaterialId] = { value: regVal.value, isInvalid: true } - setRegexValue(_regexValue) - } - } - - const handleRegexInputValueChange = (id: number, value: string, mat: CIMaterialType) => { - const _regexValue = { ...regexValue } - _regexValue[id] = { value, isInvalid: mat.regex && !new RegExp(mat.regex).test(value) } - setRegexValue(_regexValue) - } - - const renderMainContent = (): JSX.Element => { - if (showRegexModal) { - const selectedCIPipeline = selectedApp.filteredCIPipelines?.find( - (_ciPipeline) => _ciPipeline?.id == selectedApp.ciPipelineId, - ) - return ( - - ) - } - if (selectedApp.isLinkedCD) { - return ( - - ) - } - if (selectedApp.isLinkedCI) { - return ( - - ) - } - if (selectedApp.isWebhookCI) { - return ( - - ) - } - - const selectedMaterial = selectedMaterialList?.find((mat) => mat.isSelected) - - return ( - - ) - } - - const handleChange = (e): void => { - const _appIgnoreCache = { ...appIgnoreCache } - _appIgnoreCache[selectedApp.ciPipelineId] = !_appIgnoreCache[selectedApp.ciPipelineId] - setAppIgnoreCache(_appIgnoreCache) - } - - const tippyContent = (tippyTile: string, tippyDescription: string): JSX.Element => { - return ( -
-
{tippyTile}
-
{tippyDescription}
-
- ) - } - - const renderTippy = (infoText: string, tippyTile: string, tippyDescription: string): JSX.Element | null => { - return ( - -
- - {infoText} -
-
- ) - } - - const renderCacheSection = (): JSX.Element | null => { - if (!getIsAppUnorthodox(selectedApp) && !showRegexModal) { - if (selectedApp.isFirstTrigger) { - return renderTippy( - BULK_CI_MESSAGING.isFirstTrigger.infoText, - BULK_CI_MESSAGING.isFirstTrigger.title, - BULK_CI_MESSAGING.isFirstTrigger.subTitle, - ) - } - if (!selectedApp.isCacheAvailable) { - return renderTippy( - BULK_CI_MESSAGING.cacheNotAvailable.infoText, - BULK_CI_MESSAGING.cacheNotAvailable.title, - BULK_CI_MESSAGING.cacheNotAvailable.subTitle, - ) - } - if (blobStorageConfiguration?.result.enabled) { - return ( -
- - -
- ) - } - return null - } - } - - const _refreshMaterial = (pipelineId: number, gitMaterialId: number) => { - abortControllerRef.current = new AbortController() - refreshMaterial(pipelineId, gitMaterialId, abortControllerRef.current) - } - - const renderSelectedAppMaterial = (appId: number, selectedMaterialList: any[]): JSX.Element | null => { - if (appId === selectedApp.appId && !!selectedMaterialList.length && !showRegexModal) { - return ( - <> - - {renderCacheSection()} - - ) - } - return null - } - - const renderAppName = (app: BulkCIDetailType, index: number): JSX.Element | null => { - const nodeType: CommonNodeAttr['type'] = 'CI' - - return ( -
- {app.name} - {app.warningMessage && ( - - - {app.warningMessage} - - )} - {app.appId !== selectedApp.appId && app.errorMessage && ( - - - {app.errorMessage} - - )} - {appPolicy[app.appId] && PolicyEnforcementMessage && ( - - )} -
- ) - } - - const renderBodySection = (): JSX.Element => { - if (isLoading) { - const message = isBulkBuildTriggered.current - ? BULK_CI_BUILD_STATUS(appList.length) - : BULK_CI_MATERIAL_STATUS(appList.length) - return ( - - ) - } - - const sidebarTabs = Object.values(CIMaterialSidebarType).map((tabValue) => ({ - value: tabValue, - label: tabValue, - })) - - return isWebhookBulkCI ? ( - renderMainContent() - ) : ( -
-
-
- {RuntimeParamTabs ? ( - - ) : ( - 'Applications' - )} -
- - {appList.map((app, index) => ( -
- {renderAppName(app, index)} - {renderSelectedAppMaterial(app.appId, selectedMaterialList)} -
- ))} -
-
{renderMainContent()}
-
- ) - } - - const onClickStartBuild = (e: React.MouseEvent): void => { - isBulkBuildTriggered.current = true - e.stopPropagation() - onClickTriggerBulkCI(appIgnoreCache) - } - - const onClickRetryBuild = (appsToRetry: Record): void => { - onClickTriggerBulkCI(appIgnoreCache, appsToRetry) - } - - const isStartBuildDisabled = (): boolean => { - return appList.some( - (app) => - appPolicy[app.appId]?.action === ConsequenceAction.BLOCK || - (app.errorMessage && - (app.errorMessage !== SOURCE_NOT_CONFIGURED || - !app.material.some( - (_mat) => !_mat.isBranchError && !_mat.isRepoError && !_mat.isMaterialSelectionError, - ))), - ) - } - - const renderFooterSection = (): JSX.Element => { - const blobStorageNotConfigured = !blobStorageConfigurationLoading && !blobStorageConfiguration?.result?.enabled - return ( -
- {blobStorageNotConfigured && ( -
- -
-
{IGNORE_CACHE_INFO.BlobStorageNotConfigured.title}
-
- {IGNORE_CACHE_INFO.BlobStorageNotConfigured.infoText}  - -
-
-
- )} -
- ) - } - - const responseListLength = responseList.length - - return ( - -
-
- {renderHeaderSection()} - {responseListLength ? ( - - ) : ( - renderBodySection() - )} -
- {!isWebhookBulkCI && - (responseListLength ? ( - - ) : ( - renderFooterSection() - ))} -
-
- ) -} - -export default BulkCITrigger diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.scss b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.scss index 49a347b4dd..5a776df088 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.scss +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.scss @@ -44,36 +44,32 @@ justify-content: space-between; .bulk-ci-trigger { - display: grid; - grid-template-columns: 234px 1fr; - overflow: hidden; - flex-grow: 1; - - .sidebar { - .material-list { - .material-list__item { - padding: 10px; - border-radius: 4px; - border: 1px solid var(--N200); - margin-bottom: 6px; - background: var(--bg-primary); - &.material-selected { - box-shadow: none; - border-color: var(--B500); - } + .material-list { + .material-list__item { + padding: 10px; + border-radius: 4px; + border: 1px solid var(--N200); + margin-bottom: 6px; + background-color: var(--bg-primary); + + &.material-selected { + box-shadow: none; + border-color: var(--B500); } } } + .main-content { .select-material--regex-body { height: auto; } + .material-history { width: auto; } } } - + .tippy-over { .tippy-box { top: 47px; @@ -82,6 +78,7 @@ .response-list-container { overflow: auto; + .response-row { display: grid; grid-template-columns: repeat(2, 200px) auto; @@ -92,4 +89,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 473aede8b0..83be81921b 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -14,162 +14,89 @@ * limitations under the License. */ -import React, { useState, useEffect, useRef } from 'react' +import React, { useEffect, useState } from 'react' import { Prompt, Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom' -import ReactGA from 'react-ga4' +import Tippy from '@tippyjs/react' + import { - CDMaterialResponseType, + Button, + ButtonStyleType, + ButtonVariantType, + Checkbox, + CHECKBOX_VALUE, + CommonNodeAttr, + ComponentSizeType, + DEFAULT_ROUTE_PROMPT_MESSAGE, DeploymentNodeType, - ServerErrors, ErrorScreenManager, PopupMenu, Progressing, + ServerErrors, showError, - stopPropagation, sortCallback, - Checkbox, - CHECKBOX_VALUE, - VisibleModal, - WorkflowNodeType, - CommonNodeAttr, - WorkflowType, - abortPreviousRequests, - getIsRequestAborted, - handleUTCTime, - createGitCommitUrl, - CIMaterialType, - ApiQueuingWithBatch, - usePrompt, - SourceTypeMap, ToastManager, ToastVariantType, - BlockedStateData, - getStageTitle, - TriggerBlockType, - RuntimePluginVariables, - CIPipelineNodeType, - DEFAULT_ROUTE_PROMPT_MESSAGE, - triggerCDNode, - Button, - ButtonStyleType, - ButtonVariantType, - ComponentSizeType, - API_STATUS_CODES, - PipelineIdsVsDeploymentStrategyMap, - DeploymentStrategyTypeWithDefault, + usePrompt, + WorkflowNodeType, + WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' -import Tippy from '@tippyjs/react' -import { BUILD_STATUS, DEFAULT_GIT_BRANCH_VALUE, NO_COMMIT_SELECTED, URLS, ViewType } from '../../../../config' -import CDMaterial from '../../../app/details/triggerView/cdMaterial' -import { TriggerViewContext } from '../../../app/details/triggerView/config' -import { - CIMaterialProps, - CIMaterialRouterProps, - MATERIAL_TYPE, - RuntimeParamsErrorState, -} from '../../../app/details/triggerView/types' -import { Workflow } from '../../../app/details/triggerView/workflow/Workflow' -import { - getCIMaterialList, - getGitMaterialByCommitHash, - refreshGitMaterial, - triggerCINode, - triggerBranchChange, -} from '../../../app/service' -import { getCDPipelineURL, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../../common' + +import { BuildImageModal, BulkBuildImageModal } from '@Components/app/details/triggerView/BuildImageModal' +import CDMaterial from '@Components/app/details/triggerView/CDMaterial' +import { BulkDeployModal } from '@Components/app/details/triggerView/DeployImageModal' +import { shouldRenderWebhookAddImageModal } from '@Components/app/details/triggerView/TriggerView.utils' +import { getExternalCIConfig } from '@Components/ciPipeline/Webhook/webhook.service' + +import { ReactComponent as Dropdown } from '../../../../assets/icons/ic-chevron-down.svg' +import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg' +import { ReactComponent as DeployIcon } from '../../../../assets/icons/ic-nav-rocket.svg' import { ReactComponent as Pencil } from '../../../../assets/icons/ic-pencil.svg' -import { getWorkflows, getWorkflowStatus } from '../../AppGroup.service' -import { - CI_MATERIAL_EMPTY_STATE_MESSAGING, - TIME_STAMP_ORDER, - TRIGGER_VIEW_PARAMS, -} from '../../../app/details/triggerView/Constants' -import { CI_CONFIGURED_GIT_MATERIAL_ERROR } from '../../../../config/constantMessaging' -import { getCIWebhookRes } from '../../../app/details/triggerView/ciWebhook.service' +import { URLS, ViewType } from '../../../../config' +import { LinkedCIDetail } from '../../../../Pages/Shared/LinkedCIDetailsModal' import { AppNotConfigured } from '../../../app/details/appDetails/AppDetails' -import { - BULK_CI_RESPONSE_STATUS_TEXT, - BulkResponseStatus, - ENV_TRIGGER_VIEW_GA_EVENTS, - BULK_CD_RESPONSE_STATUS_TEXT, - BULK_VIRTUAL_RESPONSE_STATUS, - GetBranchChangeStatus, - SKIPPED_RESOURCES_STATUS_TEXT, - SKIPPED_RESOURCES_MESSAGE, -} from '../../Constants' -import { ReactComponent as DeployIcon } from '../../../../assets/icons/ic-nav-rocket.svg' -import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg' -import { ReactComponent as Dropdown } from '../../../../assets/icons/ic-chevron-down.svg' -import { ReactComponent as CloseIcon } from '../../../../assets/icons/ic-close.svg' -import './EnvTriggerView.scss' -import BulkCDTrigger from './BulkCDTrigger' -import BulkCITrigger from './BulkCITrigger' +import { TRIGGER_VIEW_PARAMS } from '../../../app/details/triggerView/Constants' +import { CIMaterialRouterProps, MATERIAL_TYPE } from '../../../app/details/triggerView/types' +import { Workflow } from '../../../app/details/triggerView/workflow/Workflow' +import { triggerBranchChange } from '../../../app/service' +import { importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../../common' +import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' +import { getWorkflows, getWorkflowStatus } from '../../AppGroup.service' import { AppGroupDetailDefaultType, - BulkCDDetailType, - BulkCDDetailTypeResponse, - BulkCIDetailType, ProcessWorkFlowStatusType, ResponseRowType, - TriggerVirtualEnvResponseRowType, WorkflowAppSelectionType, - WorkflowNodeSelectionType, } from '../../AppGroup.types' +import { processWorkflowStatuses } from '../../AppGroup.utils' import { - getBranchValues, - handleSourceNotConfigured, - processConsequenceData, - processWorkflowStatuses, -} from '../../AppGroup.utils' -import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' + BulkResponseStatus, + GetBranchChangeStatus, + SKIPPED_RESOURCES_MESSAGE, + SKIPPED_RESOURCES_STATUS_TEXT, +} from '../../Constants' import BulkSourceChange from './BulkSourceChange' -import { CIPipelineBuildType } from '../../../ciPipeline/types' -import { LinkedCIDetail } from '../../../../Pages/Shared/LinkedCIDetailsModal' -import CIMaterialModal from '../../../app/details/triggerView/CIMaterialModal' -import { RenderCDMaterialContentProps } from './types' -import { WebhookReceivedPayloadModal } from '@Components/app/details/triggerView/WebhookReceivedPayloadModal' -import { getExternalCIConfig } from '@Components/ciPipeline/Webhook/webhook.service' -import { shouldRenderWebhookAddImageModal } from '@Components/app/details/triggerView/TriggerView.utils' -import { getSelectedCDNode } from './utils' +import { getSelectedNodeAndAppId } from './utils' + +import './EnvTriggerView.scss' const ApprovalMaterialModal = importComponentFromFELibrary('ApprovalMaterialModal') -const getCIBlockState: (...props) => Promise = importComponentFromFELibrary( - 'getCIBlockState', - null, - 'function', -) -const getRuntimeParams = importComponentFromFELibrary('getRuntimeParams', null, 'function') const processDeploymentWindowStateAppGroup = importComponentFromFELibrary( 'processDeploymentWindowStateAppGroup', null, 'function', ) -const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) const ChangeImageSource = importComponentFromFELibrary('ChangeImageSource', null, 'function') const WebhookAddImageModal = importComponentFromFELibrary('WebhookAddImageModal', null, 'function') -// FIXME: IN CIMaterials we are sending isCDLoading while in CD materials we are sending isCILoading let inprogressStatusTimer -export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultType) { +const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultType) => { const { envId } = useParams<{ envId: string }>() const location = useLocation() const history = useHistory() const match = useRouteMatch() const { url } = useRouteMatch() - // ref to make sure that on initial mount after we fetch workflows we handle modal based on url - const handledLocation = useRef(false) - const abortControllerRef = useRef(new AbortController()) - const abortCIBuildRef = useRef(new AbortController()) - const [pageViewType, setPageViewType] = useState(ViewType.LOADING) - const [isCILoading, setCILoading] = useState(false) - const [isCDLoading, setCDLoading] = useState(false) const [isBranchChangeLoading, setIsBranchChangeLoading] = useState(false) const [showPreDeployment, setShowPreDeployment] = useState(false) const [showPostDeployment, setShowPostDeployment] = useState(false) @@ -177,40 +104,19 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou const [showBulkCDModal, setShowBulkCDModal] = useState(false) const [showBulkCIModal, setShowBulkCIModal] = useState(false) const [showBulkSourceChangeModal, setShowBulkSourceChangeModal] = useState(false) - const [isWebhookPayloadLoading, setWebhookPayloadLoading] = useState(false) - const [invalidateCache, setInvalidateCache] = useState(false) - const [webhookPayloads, setWebhookPayloads] = useState(null) - const [isChangeBranchClicked, setChangeBranchClicked] = useState(false) - const [webhookTimeStampOrder, setWebhookTimeStampOrder] = useState('') - const [showMaterialRegexModal, setShowMaterialRegexModal] = useState(false) - const [workflowID, setWorkflowID] = useState() const [selectedAppList, setSelectedAppList] = useState([]) const [workflows, setWorkflows] = useState([]) const [filteredWorkflows, setFilteredWorkflows] = useState([]) - const [selectedCDNode, setSelectedCDNode] = useState(null) - const [selectedCINode, setSelectedCINode] = useState(null) const [filteredCIPipelines, setFilteredCIPipelines] = useState(null) const [bulkTriggerType, setBulkTriggerType] = useState(null) - const [materialType, setMaterialType] = useState(MATERIAL_TYPE.inputMaterialList) const [responseList, setResponseList] = useState([]) const [isSelectAll, setSelectAll] = useState(false) const [selectAllValue, setSelectAllValue] = useState(CHECKBOX_VALUE.CHECKED) - // Mapping pipelineId (in case of CI) and appId (in case of CD) to runtime params - const [runtimeParams, setRuntimeParams] = useState>({}) - const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState>({}) - const [isBulkTriggerLoading, setIsBulkTriggerLoading] = useState(false) const [selectedWebhookNode, setSelectedWebhookNode] = useState<{ appId: number; id: number }>(null) - const [bulkDeploymentStrategy, setBulkDeploymentStrategy] = useState('DEFAULT') - const enableRoutePrompt = isBranchChangeLoading || isBulkTriggerLoading + const enableRoutePrompt = isBranchChangeLoading usePrompt({ shouldPrompt: enableRoutePrompt }) - useEffect(() => { - return () => { - handledLocation.current = false - } - }, []) - useEffect(() => { if (envId) { setPageViewType(ViewType.LOADING) @@ -228,87 +134,6 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou getWorkflowsData() } - useEffect(() => { - if (!handledLocation.current && filteredWorkflows?.length) { - handledLocation.current = true - // Would have been better if filteredWorkflows had default value to null since we are using it as a flag - // URL Encoding for Bulk is not planned as of now - setShowBulkCDModal(false) - if (location.search.includes('approval-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('approval-node')) - if (!isNaN(nodeId)) { - onClickCDMaterial(nodeId, DeploymentNodeType.CD, true) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } else if (location.search.includes('rollback-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('rollback-node')) - if (!isNaN(nodeId)) { - onClickRollbackMaterial(nodeId) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } else if (location.search.includes('cd-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('cd-node')) - const nodeType = searchParams.get('node-type') ?? DeploymentNodeType.CD - - if ( - nodeType !== DeploymentNodeType.CD && - nodeType !== DeploymentNodeType.PRECD && - nodeType !== DeploymentNodeType.POSTCD - ) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node type', - }) - history.push({ - search: '', - }) - } else if (!isNaN(nodeId)) { - onClickCDMaterial(nodeId, nodeType as DeploymentNodeType) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } else if (location.pathname.includes('build')) { - const ciNodeId = location.pathname.match(/build\/(\d+)/)?.[1] ?? null - const ciNode = filteredWorkflows - .flatMap((workflow) => workflow.nodes) - .find((node) => node.type === CIPipelineNodeType.CI && node.id === ciNodeId) - const pipelineName = ciNode?.title - - if (!isNaN(+ciNodeId) && !!pipelineName) { - onClickCIMaterial(ciNodeId, pipelineName, false) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid Node', - }) - } - } - } - }, [filteredWorkflows]) - const preserveSelection = (_workflows: WorkflowType[]) => { if (!workflows || !_workflows) { return @@ -322,27 +147,12 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou }) } - const getWorkflowsData = async (): Promise => { + const getWorkflowsData = async (): Promise => { try { const { workflows: _workflows, filteredCIPipelines } = await getWorkflows(envId, filteredAppIds) if (processDeploymentWindowStateAppGroup && _workflows.length) { await processDeploymentWindowStateAppGroup(_workflows) } - if (selectedCINode?.id) { - _workflows.forEach((wf) => - wf.nodes.forEach((n) => { - if (+n.id === selectedCINode.id) { - workflows.forEach((sw) => - sw.nodes.forEach((sn) => { - if (+sn.id === selectedCINode.id) { - n.inputMaterialList = sn.inputMaterialList - } - }), - ) - } - }), - ) - } preserveSelection(_workflows) setWorkflows(_workflows) setFilteredCIPipelines(filteredCIPipelines) @@ -350,9 +160,11 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou setPageViewType(ViewType.FORM) getWorkflowStatusData(_workflows) processFilteredData(_workflows) + + return _workflows } catch (error) { showError(error) - setErrorCode(error['code']) + setErrorCode(error.code) setPageViewType(ViewType.ERROR) } } @@ -404,7 +216,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou ) } - const getWorkflowStatusData = (workflowsList: WorkflowType[]) => { + const getWorkflowStatusData = (workflowsList: WorkflowType[] = workflows) => { getWorkflowStatus(envId, filteredAppIds) .then((response) => { const _processedWorkflowsData = processWorkflowStatuses( @@ -522,833 +334,104 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou ) } - const getCommitHistory = ( - ciPipelineMaterialId: number, - commitHash: string, - workflows: WorkflowType[], - _selectedMaterial: CIMaterialType, - ) => { - abortPreviousRequests( - () => - getGitMaterialByCommitHash( - ciPipelineMaterialId.toString(), - commitHash, - abortControllerRef.current.signal, - ), - abortControllerRef, - ) - .then((response) => { - const _result = response.result - if (_result) { - _selectedMaterial.history = [ - { - commitURL: _selectedMaterial.gitURL - ? createGitCommitUrl(_selectedMaterial.gitURL, _result.Commit) - : '', - commit: _result.Commit || '', - author: _result.Author || '', - date: _result.Date ? handleUTCTime(_result.Date, false) : '', - message: _result.Message || '', - changes: _result.Changes || [], - showChanges: true, - webhookData: _result.WebhookData, - isSelected: !_result.Excluded, - excluded: _result.Excluded, - }, - ] - _selectedMaterial.isMaterialLoading = false - _selectedMaterial.showAllCommits = false - _selectedMaterial.isMaterialSelectionError = _selectedMaterial.history[0].excluded - _selectedMaterial.materialSelectionErrorMsg = _selectedMaterial.history[0].excluded - ? NO_COMMIT_SELECTED - : '' - } else { - _selectedMaterial.history = [] - _selectedMaterial.noSearchResultsMsg = `Commit not found for ‘${commitHash}’ in branch ‘${_selectedMaterial.value}’` - _selectedMaterial.noSearchResult = true - _selectedMaterial.isMaterialLoading = false - _selectedMaterial.showAllCommits = false - _selectedMaterial.isMaterialSelectionError = true - _selectedMaterial.materialSelectionErrorMsg = NO_COMMIT_SELECTED - } - setFilteredWorkflows([...workflows]) - }) - .catch((error: ServerErrors) => { - if (!getIsRequestAborted(error)) { - showError(error) - _selectedMaterial.isMaterialLoading = false - setFilteredWorkflows([...workflows]) - } - }) - } - - const getMaterialHistoryWrapper = (nodeId: string, gitMaterialId: number, showExcluded: boolean) => - abortPreviousRequests( - () => getMaterialHistory(nodeId, abortControllerRef.current.signal, gitMaterialId, showExcluded), - abortControllerRef, - ).catch((errors: ServerErrors) => { - if (!getIsRequestAborted(errors)) { - showError(errors) - } - }) - - const getMaterialByCommit = async ( - _ciNodeId: number, - ciPipelineMaterialId: number, - gitMaterialId: number, - commitHash = null, - ) => { - let _selectedMaterial - const _workflows = [...filteredWorkflows].map((workflow) => { - workflow.nodes.map((node) => { - if (node.type === 'CI' && +node.id == _ciNodeId) { - node.inputMaterialList = node.inputMaterialList.map((material) => { - if (material.isSelected && material.searchText !== commitHash) { - material.isMaterialLoading = true - material.showAllCommits = false - material.searchText = commitHash - _selectedMaterial = material - } - return material - }) - return node - } - return node - }) - return workflow - }) - - if (commitHash && _selectedMaterial) { - const commitInLocalHistory = _selectedMaterial.history.find((material) => material.commit === commitHash) - if (commitInLocalHistory) { - _selectedMaterial.history = [{ ...commitInLocalHistory, isSelected: !commitInLocalHistory.excluded }] - _selectedMaterial.isMaterialLoading = false - _selectedMaterial.showAllCommits = false - setFilteredWorkflows(_workflows) - } else { - setFilteredWorkflows(_workflows) - getCommitHistory(ciPipelineMaterialId, commitHash, _workflows, _selectedMaterial) - } - } else { - setFilteredWorkflows(_workflows) - getMaterialHistoryWrapper(selectedCINode.id.toString(), gitMaterialId, false) - } - } + const isBuildAndBranchTriggerAllowed = (node: CommonNodeAttr): boolean => + !node.isLinkedCI && !node.isLinkedCD && node.type !== WorkflowNodeType.WEBHOOK - const getFilteredMaterial = async (ciNodeId: number, gitMaterialId: number, showExcluded: boolean) => { - const _workflows = [...filteredWorkflows].map((wf) => { - wf.nodes = wf.nodes.map((node) => { - if (node.id === ciNodeId.toString() && node.type === 'CI') { - node.inputMaterialList = node.inputMaterialList.map((material) => { - if (material.gitMaterialId === gitMaterialId) { - material.isMaterialLoading = true - material.showAllCommits = showExcluded - } - return material - }) - return node - } - return node - }) - return wf - }) - setFilteredWorkflows(_workflows) - getMaterialHistoryWrapper(ciNodeId.toString(), gitMaterialId, showExcluded) - } + const changeBranch = (value): void => { + const appIds = [] + const skippedResources = [] + const appNameMap = new Map() - const getMaterialHistory = ( - ciNodeId: string, - abortSignal: AbortSignal, - gitMaterialId?: number, - showExcluded?: boolean, - ) => { - const params = { - pipelineId: ciNodeId, - materialId: gitMaterialId, - showExcluded, - } - return getCIMaterialList(params, abortSignal).then((response) => { - let showRegexModal = false - const _workflows = [...filteredWorkflows].map((wf) => { - wf.nodes.map((node) => { - if (node.type === 'CI' && node.id == ciNodeId) { - const selectedCIPipeline = filteredCIPipelines - .get(wf.appId) - ?.find((_ci) => _ci.id === +ciNodeId) - if (selectedCIPipeline?.ciMaterial) { - for (const mat of selectedCIPipeline.ciMaterial) { - if (mat.isRegex && mat.gitMaterialId === response.result[0].gitMaterialId) { - node.isRegex = !!response.result[0].regex - if (response.result[0].value) { - node.branch = response.result[0].value - } else { - showRegexModal = !response.result[0].value - } - break - } - } - } - node.inputMaterialList = node.inputMaterialList.map((mat) => { - if (mat.id === response.result[0].id) { - return { - ...response.result[0], - isSelected: mat.isSelected, - isMaterialLoading: false, - searchText: mat.searchText, - showAllCommits: showExcluded, - } - } - return mat + filteredWorkflows.forEach((wf) => { + if (wf.isSelected) { + const _ciNode = wf.nodes.find( + (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, + ) + if (_ciNode) { + if (isBuildAndBranchTriggerAllowed(_ciNode)) { + appIds.push(wf.appId) + appNameMap.set(wf.appId, wf.name) + } else { + skippedResources.push({ + appId: wf.appId, + appName: wf.name, + statusText: SKIPPED_RESOURCES_STATUS_TEXT, + status: BulkResponseStatus.SKIP, + envId: +envId, + message: SKIPPED_RESOURCES_MESSAGE, }) } - return node - }) - wf.tagsEditable = response.result.tagsEditable - wf.appReleaseTags = response.result.appReleaseTags - return wf - }) - setFilteredWorkflows(_workflows) - if (!showBulkCIModal) { - setShowMaterialRegexModal(showRegexModal) - } - getWorkflowStatusData(_workflows) - }) - } - - // NOTE: GIT MATERIAL ID - const refreshMaterial = (ciNodeId: number, gitMaterialId: number, abortController?: AbortController) => { - let showExcluded = false - const _workflows = [...filteredWorkflows].map((wf) => { - wf.nodes = wf.nodes.map((node) => { - if (node.id === ciNodeId.toString() && node.type === 'CI') { - node.inputMaterialList = node.inputMaterialList.map((material) => { - if (material.gitMaterialId === gitMaterialId) { - material.isMaterialLoading = true - showExcluded = material.showAllCommits - } - return material - }) - return node - } - return node - }) - return wf - }) - setFilteredWorkflows(_workflows) - - // Would be only aborting the calls before refreshGitMaterial and not the subsequent calls - abortPreviousRequests( - () => refreshGitMaterial(gitMaterialId.toString(), abortControllerRef.current.signal), - abortControllerRef, - ) - .then((response) => { - getMaterialHistory( - ciNodeId.toString(), - abortControllerRef.current.signal, - gitMaterialId, - showExcluded, - ).catch((errors: ServerErrors) => { - if (!getIsRequestAborted(errors)) { - showError(errors) - } - }) - }) - .catch((error: ServerErrors) => { - if (!getIsRequestAborted(error)) { - showError(error) } - }) - } - - const updateCIMaterialList = async ( - ciNodeId: string, - ciPipelineName: string, - preserveMaterialSelection: boolean, - abortSignal: AbortSignal, - ): Promise => { - const params = { - pipelineId: ciNodeId, - } - return getCIMaterialList(params, abortSignal).then((response) => { - let _workflowId - let _appID - let showRegexModal = false - const _workflows = [...filteredWorkflows].map((workflow) => { - workflow.nodes.map((node) => { - if (node.type === 'CI' && node.id == ciNodeId) { - _workflowId = workflow.id - _appID = workflow.appId - const selectedCIPipeline = filteredCIPipelines.get(_appID)?.find((_ci) => _ci.id === +ciNodeId) - if (selectedCIPipeline?.ciMaterial) { - for (const mat of selectedCIPipeline.ciMaterial) { - const gitMaterial = response.result.find( - (_mat) => _mat.gitMaterialId === mat.gitMaterialId, - ) - if (mat.isRegex && gitMaterial) { - node.branch = gitMaterial.value - node.isRegex = !!gitMaterial.regex - } - } - } - if (preserveMaterialSelection) { - const selectMaterial = node.inputMaterialList.find((mat) => mat.isSelected) - node.inputMaterialList = response.result.map((material) => { - return { - ...material, - isSelected: selectMaterial.id === material.id, - } - }) - } else { - node.inputMaterialList = response.result - } - return node - } - return node - }) - return workflow - }) - showRegexModal = isShowRegexModal(_appID, +ciNodeId, response.result) - setFilteredWorkflows(_workflows) - setErrorCode(response.code) - setSelectedCINode({ id: +ciNodeId, name: ciPipelineName, type: WorkflowNodeType.CI }) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - if (!showBulkCIModal) { - setShowMaterialRegexModal(showRegexModal) } - setWorkflowID(_workflowId) - getWorkflowStatusData(_workflows) }) - } - const isShowRegexModal = (_appId: number, ciNodeId: number, inputMaterialList: any[]): boolean => { - let showRegexModal = false - const selectedCIPipeline = filteredCIPipelines.get(_appId).find((_ci) => _ci.id === ciNodeId) - if (selectedCIPipeline?.ciMaterial) { - for (const mat of selectedCIPipeline.ciMaterial) { - showRegexModal = inputMaterialList.some((_mat) => { - return _mat.gitMaterialId === mat.gitMaterialId && mat.isRegex && !_mat.value - }) - if (showRegexModal) { - break - } - } + if (!appIds.length && !skippedResources.length) { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'No valid application present', + }) + return } - return showRegexModal - } + setIsBranchChangeLoading(true) - const onClickCIMaterial = (ciNodeId: string, ciPipelineName: string, preserveMaterialSelection: boolean) => { - setCILoading(true) - history.push(`${url}${URLS.BUILD}/${ciNodeId}`) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - setWebhookPayloads(null) - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.MaterialClicked) - abortControllerRef.current.abort() - abortControllerRef.current = new AbortController() - let _appID - let _appName - for (const _wf of filteredWorkflows) { - const nd = _wf.nodes.find((node) => +node.id == +ciNodeId && node.type === 'CI') - if (nd) { - _appID = _wf.appId - _appName = _wf.name - break - } + if (!appIds.length) { + updateResponseListData(skippedResources) + setIsBranchChangeLoading(false) + return } - Promise.all([ - updateCIMaterialList( - ciNodeId, - ciPipelineName, - preserveMaterialSelection, - abortControllerRef.current.signal, - ), - getCIBlockState - ? getCIBlockState( - ciNodeId, - _appID, - getBranchValues(ciNodeId, filteredWorkflows, filteredCIPipelines.get(_appID)), - _appName, - ) - : null, - getRuntimeParams ? getRuntimeParams(ciNodeId) : null, - ]) - .then((resp) => { - // need to set result for getCIBlockState call only as for updateCIMaterialList - // it's already being set inside the same function - if (resp[1]) { - const workflows = [...filteredWorkflows].map((workflow) => { - workflow.nodes.map((node) => { - if (node.type === 'CI' && node.id == ciNodeId) { - node.pluginBlockState = processConsequenceData(resp[1]) - node.isTriggerBlocked = resp[1].isCITriggerBlocked - return node - } - return node - }) - return workflow - }) - setFilteredWorkflows(workflows) - } - - if (resp[2]) { - // Not handling error state since we are change viewType to error in catch block - setRuntimeParams({ - [ciNodeId]: resp[2], + triggerBranchChange(appIds, +envId, value) + .then((response: any) => { + const _responseList = [] + response.map((res) => { + _responseList.push({ + appId: res.appId, + appName: appNameMap.get(res.appId), + statusText: res.status, + status: GetBranchChangeStatus(res.status), + envId: +envId, + message: res.message, }) - } + }) + updateResponseListData([..._responseList, ...skippedResources]) }) - .catch((errors: ServerErrors) => { - if (!getIsRequestAborted(errors)) { - showError(errors) - } - closeCIModal() + .catch((error) => { + showError(error) }) .finally(() => { - setCILoading(false) + setIsBranchChangeLoading(false) }) } - const onClickCDMaterial = (cdNodeId, nodeType: DeploymentNodeType, isApprovalNode: boolean = false): void => { - ReactGA.event( - isApprovalNode ? ENV_TRIGGER_VIEW_GA_EVENTS.ApprovalNodeClicked : ENV_TRIGGER_VIEW_GA_EVENTS.ImageClicked, - ) - - let _workflowId - let _appID - let _selectedNode - - // FIXME: This needs to be replicated in rollback, env group since we need cipipelineid as 0 in external case - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (cdNodeId == node.id && node.type === nodeType) { - // TODO: Ig not using this, can remove it - if (node.type === WorkflowNodeType.CD) { - node.approvalConfigData = workflow.approvalConfiguredIdsMap[cdNodeId] - } - _selectedNode = node - _workflowId = workflow.id - _appID = workflow.appId - } - return node - }) - workflow.nodes = nodes - return workflow + const closeApprovalModal = (e: React.MouseEvent): void => { + e.stopPropagation() + history.push({ + search: '', }) + getWorkflowStatusData(workflows) + } - if (!_selectedNode) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - return - } - - setWorkflowID(_workflowId) - setFilteredWorkflows(_workflows) - setSelectedCDNode({ id: +cdNodeId, name: _selectedNode.name, type: _selectedNode.type }) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - - const newParams = new URLSearchParams(location.search) - newParams.set(isApprovalNode ? 'approval-node' : 'cd-node', cdNodeId.toString()) - if (!isApprovalNode) { - newParams.set('node-type', nodeType) - } else { - const currentApprovalState = newParams.get(TRIGGER_VIEW_PARAMS.APPROVAL_STATE) - // If the current state is pending, then we should change the state to pending - const approvalState = - currentApprovalState === TRIGGER_VIEW_PARAMS.PENDING - ? TRIGGER_VIEW_PARAMS.PENDING - : TRIGGER_VIEW_PARAMS.APPROVAL + const hideBulkCDModal = () => { + setShowBulkCDModal(false) + setResponseList([]) - newParams.set(TRIGGER_VIEW_PARAMS.APPROVAL_STATE, approvalState) - newParams.delete(TRIGGER_VIEW_PARAMS.CD_NODE) - newParams.delete(TRIGGER_VIEW_PARAMS.NODE_TYPE) - } history.push({ - search: newParams.toString(), + search: '', }) } - const onClickRollbackMaterial = (cdNodeId: number) => { - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.RollbackClicked) - - let _selectedNode - - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (node.type === 'CD' && +node.id == cdNodeId) { - node.approvalConfigData = workflow.approvalConfiguredIdsMap[cdNodeId] - _selectedNode = node - } - return node - }) - workflow.nodes = nodes - return workflow - }) - - if (!_selectedNode) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - return - } - - setFilteredWorkflows(_workflows) - setSelectedCDNode({ id: +cdNodeId, name: _selectedNode.name, type: _selectedNode.type }) - setMaterialType(MATERIAL_TYPE.rollbackMaterialList) - getWorkflowStatusData(_workflows) - - const newParams = new URLSearchParams(location.search) - newParams.set('rollback-node', cdNodeId.toString()) - history.push({ - search: newParams.toString(), - }) - } - - const onClickTriggerCINode = () => { - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.CITriggered) - setCDLoading(true) - let node - let dockerfileConfiguredGitMaterialId - for (const wf of filteredWorkflows) { - node = wf.nodes.find((node) => { - return node.type === selectedCINode.type && +node.id == selectedCINode.id - }) - - if (node) { - dockerfileConfiguredGitMaterialId = wf.ciConfiguredGitMaterialId - break - } - } - - const gitMaterials = new Map() - const ciPipelineMaterials = [] - for (const _inputMaterial of node.inputMaterialList) { - gitMaterials[_inputMaterial.gitMaterialId] = [ - _inputMaterial.gitMaterialName.toLowerCase(), - _inputMaterial.value, - ] - if (_inputMaterial) { - if (_inputMaterial.value === DEFAULT_GIT_BRANCH_VALUE) { - continue - } - const history = _inputMaterial.history.filter((hstry) => hstry.isSelected) - if (!history.length) { - history.push(_inputMaterial.history[0]) - } - - history.forEach((element) => { - const historyItem = { - Id: _inputMaterial.id, - GitCommit: { - Commit: element.commit, - }, - } - if (!element.commit) { - historyItem.GitCommit['WebhookData'] = { - id: element.webhookData.id, - } - } - ciPipelineMaterials.push(historyItem) - }) - } - } - if (gitMaterials[dockerfileConfiguredGitMaterialId][1] === DEFAULT_GIT_BRANCH_VALUE) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: CI_CONFIGURED_GIT_MATERIAL_ERROR.replace( - '$GIT_MATERIAL_ID', - `"${gitMaterials[dockerfileConfiguredGitMaterialId][0]}"`, - ), - }) - setCDLoading(false) - return - } - - // For this block validation is handled in CIMaterial - const runtimeParamsPayload = getRuntimeParamsPayload?.(runtimeParams?.[selectedCINode?.id] ?? []) - - const payload = { - pipelineId: +selectedCINode.id, - ciPipelineMaterials, - invalidateCache, - pipelineType: node.isJobCI ? CIPipelineBuildType.CI_JOB : CIPipelineBuildType.CI_BUILD, - ...(getRuntimeParamsPayload ? runtimeParamsPayload : {}), - } - - triggerCINode(payload, abortCIBuildRef.current.signal) - .then((response: any) => { - if (response.result) { - ToastManager.showToast({ - variant: ToastVariantType.success, - description: 'Pipeline Triggered', - }) - setCDLoading(false) - closeCIModal() - setErrorCode(response.code) - setInvalidateCache(false) - getWorkflowStatusData(workflows) - } - }) - .catch((errors: ServerErrors) => { - if (!getIsRequestAborted(errors)) { - showError(errors) - } - - setCDLoading(false) - - setErrorCode(errors.code) - }) - } - - const selectCommit = (materialId: string, hash: string, ciPipelineId?: string): void => { - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (node.type === WorkflowNodeType.CI && +node.id == (ciPipelineId ?? selectedCINode.id)) { - node.inputMaterialList.map((material) => { - if (material.id == materialId && material.isSelected) { - material.history.map((hist) => { - if (!hist.excluded) { - if (material.type == SourceTypeMap.WEBHOOK) { - if (hist?.webhookData && hist.webhookData?.id && hash == hist.webhookData.id) { - hist.isSelected = true - } else { - hist.isSelected = false - } - } else { - hist.isSelected = hash == hist.commit - } - } else { - hist.isSelected = false - } - }) - } - }) - return node - } - return node - }) - workflow.nodes = nodes - return workflow - }) - setFilteredWorkflows(_workflows) - } - - const isBuildAndBranchTriggerAllowed = (node: CommonNodeAttr): boolean => - !node.isLinkedCI && !node.isLinkedCD && node.type !== WorkflowNodeType.WEBHOOK - - const changeBranch = (value): void => { - const appIds = [] - const skippedResources = [] - const appNameMap = new Map() - - filteredWorkflows.forEach((wf) => { - if (wf.isSelected) { - const _ciNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - if (_ciNode) { - if (isBuildAndBranchTriggerAllowed(_ciNode)) { - appIds.push(wf.appId) - appNameMap.set(wf.appId, wf.name) - } else { - skippedResources.push({ - appId: wf.appId, - appName: wf.name, - statusText: SKIPPED_RESOURCES_STATUS_TEXT, - status: BulkResponseStatus.SKIP, - envId: +envId, - message: SKIPPED_RESOURCES_MESSAGE, - }) - } - } - } - }) - - if (!appIds.length && !skippedResources.length) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'No valid application present', - }) - return - } - setIsBranchChangeLoading(true) - - if (!appIds.length) { - updateResponseListData(skippedResources) - setIsBranchChangeLoading(false) - setCDLoading(false) - setCILoading(false) - return - } - - triggerBranchChange(appIds, +envId, value) - .then((response: any) => { - const _responseList = [] - response.map((res) => { - _responseList.push({ - appId: res.appId, - appName: appNameMap.get(res.appId), - statusText: res.status, - status: GetBranchChangeStatus(res.status), - envId: +envId, - message: res.message, - }) - }) - updateResponseListData([..._responseList, ...skippedResources]) - setCDLoading(false) - setCILoading(false) - }) - .catch((error) => { - showError(error) - }) - .finally(() => { - setIsBranchChangeLoading(false) - }) - } - - const selectMaterial = (materialId, pipelineId?: number): void => { - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (node.type === WorkflowNodeType.CI && +node.id == (pipelineId ?? selectedCINode.id)) { - node.inputMaterialList = node.inputMaterialList.map((material) => { - return { - ...material, - searchText: material.searchText || '', - isSelected: material.id == materialId, - } - }) - } - return node - }) - workflow.nodes = nodes - return workflow - }) - setFilteredWorkflows(_workflows) - } - - const toggleChanges = (materialId: string, hash: string): void => { - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (node.type === selectedCINode.type && +node.id == selectedCINode.id) { - node.inputMaterialList.map((material) => { - if (material.id == materialId) { - material.history.map((hist) => { - if (hist.commit == hash) { - hist.showChanges = !hist.showChanges - } - }) - } - }) - } - return node - }) - workflow.nodes = nodes - return workflow - }) - - setFilteredWorkflows(_workflows) - } - - const toggleInvalidateCache = (): void => { - setInvalidateCache(!invalidateCache) - } - - const closeCIModal = (): void => { - abortControllerRef.current.abort() - setShowMaterialRegexModal(false) - setRuntimeParams({}) - setRuntimeParamsErrorState({}) - history.push(url) - } - - const closeCDModal = (e: React.MouseEvent): void => { - e?.stopPropagation() - abortControllerRef.current.abort() - setCDLoading(false) - history.push({ - search: '', - }) - getWorkflowStatusData(workflows) - } - - const closeApprovalModal = (e: React.MouseEvent): void => { - e.stopPropagation() - history.push({ - search: '', - }) - getWorkflowStatusData(workflows) - } - - const onClickWebhookTimeStamp = () => { - if (webhookTimeStampOrder === TIME_STAMP_ORDER.DESCENDING) { - setWebhookTimeStampOrder(TIME_STAMP_ORDER.ASCENDING) - } else if (webhookTimeStampOrder === TIME_STAMP_ORDER.ASCENDING) { - setWebhookTimeStampOrder(TIME_STAMP_ORDER.DESCENDING) - } - } - - const getWebhookPayload = (id, _webhookTimeStampOrder) => { - setWebhookPayloadLoading(true) - getCIWebhookRes(id, _webhookTimeStampOrder).then((result) => { - setWebhookPayloads(result?.result) - setWebhookPayloadLoading(false) - }) - } - - const onCloseBranchRegexModal = () => { - setShowMaterialRegexModal(false) - } - - const onClickShowBranchRegexModal = (isChangedBranch = false) => { - setShowMaterialRegexModal(true) - setChangeBranchClicked(isChangedBranch) - } - - const hideBulkCDModal = () => { - setCDLoading(false) - setShowBulkCDModal(false) - setResponseList([]) - setBulkDeploymentStrategy('DEFAULT') - setRuntimeParams({}) - setRuntimeParamsErrorState({}) - - history.push({ - search: '', - }) - } - - const onShowBulkCDModal = (e) => { - setCDLoading(true) - setBulkTriggerType(e.currentTarget.dataset.triggerType) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - setTimeout(() => { - setShowBulkCDModal(true) - }, 100) - } + const onShowBulkCDModal = (e) => { + setBulkTriggerType(e.currentTarget.dataset.triggerType) + setShowBulkCDModal(true) + } const hideBulkCIModal = () => { - setCILoading(false) setShowBulkCIModal(false) setResponseList([]) - - setRuntimeParams({}) - setRuntimeParamsErrorState({}) } const onShowBulkCIModal = () => { - setCILoading(true) - setWebhookPayloads(null) - setTimeout(() => { - setShowBulkCIModal(true) - }, 100) + setShowBulkCIModal(true) } const hideChangeSourceModal = () => { @@ -1371,146 +454,6 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou setShowBulkSourceChangeModal(true) } - const updateBulkCDInputMaterial = (cdMaterialResponse: Record): void => { - const _workflows = filteredWorkflows.map((wf) => { - if (wf.isSelected && cdMaterialResponse[wf.appId]) { - const _appId = wf.appId - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - let _selectedNode: CommonNodeAttr - const _materialData = cdMaterialResponse[_appId] - - if (bulkTriggerType === DeploymentNodeType.PRECD) { - _selectedNode = _cdNode.preNode - } else if (bulkTriggerType === DeploymentNodeType.CD) { - _selectedNode = _cdNode - _selectedNode.requestedUserId = _materialData.requestedUserId - _selectedNode.approvalConfigData = _materialData.deploymentApprovalInfo?.approvalConfigData - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - _selectedNode = _cdNode.postNode - } - - if (_selectedNode) { - _selectedNode.inputMaterialList = _materialData.materials - } - wf.appReleaseTags = _materialData?.appReleaseTagNames - wf.tagsEditable = _materialData?.tagsEditable - wf.canApproverDeploy = _materialData?.canApproverDeploy ?? false - wf.isExceptionUser = _materialData?.deploymentApprovalInfo?.approvalConfigData?.isExceptionUser ?? false - } - - return wf - }) - setFilteredWorkflows(_workflows) - } - - const validateBulkRuntimeParams = (): boolean => { - let isRuntimeParamErrorPresent = false - - const updatedRuntimeParamsErrorState = Object.keys(runtimeParams).reduce((acc, key) => { - const validationState = validateRuntimeParameters(runtimeParams[key]) - acc[key] = validationState - isRuntimeParamErrorPresent = !isRuntimeParamErrorPresent && !validationState.isValid - return acc - }, {}) - setRuntimeParamsErrorState(updatedRuntimeParamsErrorState) - - if (isRuntimeParamErrorPresent) { - setCDLoading(false) - setCILoading(false) - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please resolve all the runtime parameter errors before triggering the pipeline', - }) - return false - } - - return true - } - - // Helper to get selected CD nodes - const getSelectedCDNodesWithArtifacts = ( - selectedWorkflows: WorkflowType[], - ): { node: CommonNodeAttr; wf: WorkflowType }[] => - selectedWorkflows - .filter((wf) => wf.isSelected) - .map((wf) => { - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - if (!_cdNode) return null - - const _selectedNode: CommonNodeAttr | undefined = getSelectedCDNode(bulkTriggerType, _cdNode) - - const selectedArtifacts = _selectedNode?.[materialType]?.filter((artifact) => artifact.isSelected) ?? [] - if (selectedArtifacts.length > 0) { - return { node: _selectedNode, wf } - } - return null - }) - .filter(Boolean) - - const onClickTriggerBulkCD = ( - skipIfHibernated: boolean, - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap, - appsToRetry?: Record, - ) => { - if (isCDLoading || !validateBulkRuntimeParams()) { - return - } - - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.BulkCDTriggered(bulkTriggerType)) - setCDLoading(true) - const _appIdMap = new Map() - const nodeList: CommonNodeAttr[] = [] - const triggeredAppList: { appId: number; envId?: number; appName: string }[] = [] - - const eligibleNodes = getSelectedCDNodesWithArtifacts( - filteredWorkflows.filter((wf) => !appsToRetry || appsToRetry[wf.appId]), - ) - eligibleNodes.forEach(({ node: eligibleNode, wf }) => { - nodeList.push(eligibleNode) - _appIdMap.set(eligibleNode.id, wf.appId.toString()) - triggeredAppList.push({ appId: wf.appId, appName: wf.name, envId: eligibleNode.environmentId }) - }) - - const _CDTriggerPromiseFunctionList = [] - nodeList.forEach((node, index) => { - let ciArtifact = null - const currentAppId = _appIdMap.get(node.id) - - node[materialType].forEach((artifact) => { - if (artifact.isSelected == true) { - ciArtifact = artifact - } - }) - const pipelineId = Number(node.id) - const strategy = pipelineIdVsStrategyMap[pipelineId] - - // skip app if bulkDeploymentStrategy is not default and strategy is not configured for app - if (ciArtifact && (bulkDeploymentStrategy === 'DEFAULT' || !!strategy)) { - _CDTriggerPromiseFunctionList.push(() => - triggerCDNode({ - pipelineId, - ciArtifactId: Number(ciArtifact.id), - appId: Number(currentAppId), - stageType: bulkTriggerType, - ...(getRuntimeParamsPayload - ? { runtimeParamsPayload: getRuntimeParamsPayload(runtimeParams[currentAppId] ?? []) } - : {}), - skipIfHibernated, - // strategy DEFAULT means custom chart - ...(strategy && strategy !== 'DEFAULT' ? { strategy } : {}), - }), - ) - } else { - triggeredAppList.splice(index, 1) - } - }) - handleBulkTrigger(_CDTriggerPromiseFunctionList, triggeredAppList, WorkflowNodeType.CD) - } - const updateResponseListData = (_responseList) => { setResponseList((prevList) => { const resultMap = new Map(_responseList.map((data) => [data.appId, data])) @@ -1521,451 +464,6 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou }) } - const filterStatusType = ( - type: WorkflowNodeType, - CIStatus: string, - VirtualStatus: string, - CDStatus: string, - ): string => { - if (type === WorkflowNodeType.CI) { - return CIStatus - } - if (isVirtualEnv) { - return VirtualStatus - } - return CDStatus - } - - const handleBulkTrigger = ( - promiseFunctionList: any[], - triggeredAppList: { appId: number; envId?: number; appName: string }[], - type: WorkflowNodeType, - skippedResources: ResponseRowType[] = [], - ): void => { - setIsBulkTriggerLoading(true) - const _responseList = skippedResources - if (promiseFunctionList.length) { - ApiQueuingWithBatch(promiseFunctionList).then((responses: any[]) => { - responses.forEach((response, index) => { - if (response.status === 'fulfilled') { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.PASS], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.PASS], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.PASS], - ) - - const virtualEnvResponseRowType: TriggerVirtualEnvResponseRowType = - [DeploymentNodeType.CD, DeploymentNodeType.POSTCD, DeploymentNodeType.PRECD].includes( - bulkTriggerType, - ) && isVirtualEnv - ? { - isVirtual: true, - helmPackageName: response.value?.result?.helmPackageName, - cdWorkflowType: bulkTriggerType, - } - : {} - - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.PASS, - envId: triggeredAppList[index].envId, - message: '', - ...virtualEnvResponseRowType, - }) - } else { - const errorReason = response.reason - if (errorReason.code === API_STATUS_CODES.EXPECTATION_FAILED) { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.SKIP], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.SKIP], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.SKIP], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.SKIP, - message: errorReason.errors[0].userMessage, - }) - } else if (errorReason.code === 403 || errorReason.code === 422) { - // Adding 422 to handle the unauthorized state due to deployment window - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.UNAUTHORIZE], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.UNAUTHORIZE], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.UNAUTHORIZE], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.UNAUTHORIZE, - message: errorReason.errors[0].userMessage, - }) - } else { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.FAIL], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.FAIL], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.FAIL], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.FAIL, - message: errorReason.errors[0].userMessage, - }) - } - } - }) - updateResponseListData(_responseList) - setCDLoading(false) - setCILoading(false) - setIsBulkTriggerLoading(false) - getWorkflowStatusData(workflows) - }) - } else { - setCDLoading(false) - setCILoading(false) - setIsBulkTriggerLoading(false) - - if (!skippedResources.length) { - hideBulkCIModal() - hideBulkCDModal() - } else { - updateResponseListData(_responseList) - } - } - } - - const updateBulkCIInputMaterial = (materialList: Record): void => { - const _workflows = [...filteredWorkflows].map((wf) => { - const _appId = wf.appId - const _ciNode = wf.nodes.find((node) => node.type === WorkflowNodeType.CI) - if (_ciNode) { - _ciNode.inputMaterialList = materialList[_appId] - } - return wf - }) - setFilteredWorkflows(_workflows) - } - - const onClickTriggerBulkCI = (appIgnoreCache: Record, appsToRetry?: Record) => { - if (isCILoading || !validateBulkRuntimeParams()) { - return - } - - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.BulkCITriggered) - setCILoading(true) - let node - const skippedResources = [] - const nodeList: CommonNodeAttr[] = [] - const triggeredAppList: { appId: number; appName: string }[] = [] - for (const _wf of filteredWorkflows) { - if (_wf.isSelected && (!appsToRetry || appsToRetry[_wf.appId])) { - node = _wf.nodes.find((node) => { - return node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK - }) - - if (node && isBuildAndBranchTriggerAllowed(node)) { - triggeredAppList.push({ appId: _wf.appId, appName: _wf.name }) - nodeList.push(node) - } else if (node && !isBuildAndBranchTriggerAllowed(node)) { - // skipped can never be in appsToRetry - skippedResources.push({ - appId: _wf.appId, - appName: _wf.name, - statusText: SKIPPED_RESOURCES_STATUS_TEXT, - status: BulkResponseStatus.SKIP, - message: SKIPPED_RESOURCES_MESSAGE, - }) - } - } - } - const _CITriggerPromiseFunctionList = [] - - nodeList.forEach((node) => { - const gitMaterials = new Map() - const ciPipelineMaterials = [] - for (let i = 0; i < node.inputMaterialList.length; i++) { - gitMaterials[node.inputMaterialList[i].gitMaterialId] = [ - node.inputMaterialList[i].gitMaterialName.toLowerCase(), - node.inputMaterialList[i].value, - ] - if (node.inputMaterialList[i].value === DEFAULT_GIT_BRANCH_VALUE) { - continue - } - const history = node.inputMaterialList[i].history.filter((hstry) => hstry.isSelected) - if (!history.length) { - history.push(node.inputMaterialList[i].history[0]) - } - - history.forEach((element) => { - const historyItem = { - Id: node.inputMaterialList[i].id, - GitCommit: { - Commit: element.commit, - }, - } - if (!element.commit) { - historyItem.GitCommit['WebhookData'] = { - id: element.webhookData.id, - } - } - ciPipelineMaterials.push(historyItem) - }) - } - - const runtimeParamsPayload = getRuntimeParamsPayload?.(runtimeParams?.[node.id] ?? []) - - const payload = { - pipelineId: +node.id, - ciPipelineMaterials, - invalidateCache: appIgnoreCache[+node.id], - pipelineType: node.isJobCI ? CIPipelineBuildType.CI_JOB : CIPipelineBuildType.CI_BUILD, - ...(getRuntimeParamsPayload ? runtimeParamsPayload : {}), - } - _CITriggerPromiseFunctionList.push(() => triggerCINode(payload)) - }) - - if (!_CITriggerPromiseFunctionList.length && !skippedResources.length) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'No valid CI pipeline found', - }) - setCDLoading(false) - setCILoading(false) - return - } - - handleBulkTrigger(_CITriggerPromiseFunctionList, triggeredAppList, WorkflowNodeType.CI, skippedResources) - } - - // Would only set data no need to get data related to materials from it, we will get that in bulk trigger - const createBulkCDTriggerData = (): BulkCDDetailTypeResponse => { - const uniqueReleaseTags: string[] = [] - const uniqueTagsSet = new Set() - const _selectedAppWorkflowList: BulkCDDetailType[] = [] - - filteredWorkflows.forEach((wf) => { - if (wf.isSelected) { - // extract unique tags for this workflow - wf.appReleaseTags?.forEach((tag) => { - if (!uniqueTagsSet.has(tag)) { - uniqueReleaseTags.push(tag) - } - uniqueTagsSet.add(tag) - }) - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - const selectedCINode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - const doesWorkflowContainsWebhook = selectedCINode?.type === WorkflowNodeType.WEBHOOK - - let _selectedNode: CommonNodeAttr - if (bulkTriggerType === DeploymentNodeType.PRECD) { - _selectedNode = _cdNode.preNode - } else if (bulkTriggerType === DeploymentNodeType.CD) { - _selectedNode = _cdNode - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - _selectedNode = _cdNode.postNode - } - if (_selectedNode) { - const stageType = DeploymentNodeType[_selectedNode.type] - const isTriggerBlockedDueToPlugin = - _selectedNode.isTriggerBlocked && _selectedNode.showPluginWarning - const isTriggerBlockedDueToMandatoryTags = - _selectedNode.isTriggerBlocked && - _selectedNode.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG - const stageText = getStageTitle(stageType) - - _selectedAppWorkflowList.push({ - workFlowId: wf.id, - appId: wf.appId, - name: wf.name, - cdPipelineName: _cdNode.title, - cdPipelineId: _cdNode.id, - stageType, - triggerType: _cdNode.triggerType, - envName: _selectedNode.environmentName, - envId: _selectedNode.environmentId, - parentPipelineId: _selectedNode.parentPipelineId, - parentPipelineType: WorkflowNodeType[_selectedNode.parentPipelineType], - parentEnvironmentName: _selectedNode.parentEnvironmentName, - material: _selectedNode.inputMaterialList, - approvalConfigData: _selectedNode.approvalConfigData, - requestedUserId: _selectedNode.requestedUserId, - appReleaseTags: wf.appReleaseTags, - tagsEditable: wf.tagsEditable, - ciPipelineId: _selectedNode.connectingCiPipelineId, - hideImageTaggingHardDelete: wf.hideImageTaggingHardDelete, - showPluginWarning: _selectedNode.showPluginWarning, - isTriggerBlockedDueToPlugin, - configurePluginURL: getCDPipelineURL( - String(wf.appId), - wf.id, - doesWorkflowContainsWebhook ? '0' : selectedCINode?.id, - doesWorkflowContainsWebhook, - _selectedNode.id, - true, - ), - consequence: _selectedNode.pluginBlockState, - warningMessage: - isTriggerBlockedDueToPlugin || isTriggerBlockedDueToMandatoryTags - ? `${stageText} is blocked` - : '', - triggerBlockedInfo: _selectedNode.triggerBlockedInfo, - isExceptionUser: wf.isExceptionUser, - }) - } else { - let warningMessage = '' - if (bulkTriggerType === DeploymentNodeType.PRECD) { - warningMessage = 'No pre-deployment stage' - } else if (bulkTriggerType === DeploymentNodeType.CD) { - warningMessage = 'No deployment stage' - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - warningMessage = 'No post-deployment stage' - } - _selectedAppWorkflowList.push({ - workFlowId: wf.id, - appId: wf.appId, - name: wf.name, - warningMessage, - envName: _cdNode.environmentName, - envId: _cdNode.environmentId, - }) - } - } - }) - _selectedAppWorkflowList.sort((a, b) => sortCallback('name', a, b)) - return { - bulkCDDetailType: _selectedAppWorkflowList, - uniqueReleaseTags, - } - } - - const getWarningMessage = (_ciNode): string => { - if (_ciNode.isLinkedCD) { - return 'Uses another environment as image source' - } - - if (_ciNode.isLinkedCI) { - return 'Has linked build pipeline' - } - - if (_ciNode.type === WorkflowNodeType.WEBHOOK) { - return 'Has webhook build pipeline' - } - } - - const getErrorMessage = (_appId, _ciNode): string => { - let errorMessage = '' - if (_ciNode.inputMaterialList?.length > 0) { - if (isShowRegexModal(_appId, +_ciNode.id, _ciNode.inputMaterialList)) { - errorMessage = 'Primary branch is not set' - } else { - const selectedCIPipeline = filteredCIPipelines.get(_appId).find((_ci) => _ci.id === +_ciNode.id) - if (selectedCIPipeline?.ciMaterial) { - const invalidInputMaterial = _ciNode.inputMaterialList.find((_mat) => { - return ( - _mat.isBranchError || - _mat.isRepoError || - _mat.isDockerFileError || - _mat.isMaterialSelectionError || - (_mat.type === SourceTypeMap.WEBHOOK && _mat.history.length === 0) - ) - }) - if (invalidInputMaterial) { - if (invalidInputMaterial.isRepoError) { - errorMessage = invalidInputMaterial.repoErrorMsg - } else if (invalidInputMaterial.isDockerFileError) { - errorMessage = invalidInputMaterial.dockerFileErrorMsg - } else if (invalidInputMaterial.isBranchError) { - errorMessage = invalidInputMaterial.branchErrorMsg - } else if (invalidInputMaterial.isMaterialSelectionError) { - errorMessage = invalidInputMaterial.materialSelectionErrorMsg - } else { - errorMessage = CI_MATERIAL_EMPTY_STATE_MESSAGING.NoMaterialFound - } - } - } - } - } - return errorMessage - } - - /** - * Acting only for single build trigger - */ - const handleRuntimeParamChange: CIMaterialProps['handleRuntimeParamChange'] = (updatedRuntimeParams) => { - if (selectedCINode?.id) { - setRuntimeParams({ [selectedCINode.id]: updatedRuntimeParams }) - } - } - - const createBulkCITriggerData = (): BulkCIDetailType[] => { - const _selectedAppWorkflowList: BulkCIDetailType[] = [] - filteredWorkflows.forEach((wf) => { - if (wf.isSelected) { - const _ciNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - if (_ciNode) { - const configuredMaterialList = new Map>() - configuredMaterialList[wf.name] = new Set() - if (!_ciNode[MATERIAL_TYPE.inputMaterialList]) { - _ciNode[MATERIAL_TYPE.inputMaterialList] = [] - } - if (!_ciNode.isLinkedCI && _ciNode.type !== WorkflowNodeType.WEBHOOK && !_ciNode.isLinkedCD) { - const gitMaterials = new Map() - for (const _inputMaterial of _ciNode.inputMaterialList) { - gitMaterials[_inputMaterial.gitMaterialId] = [ - _inputMaterial.gitMaterialName.toLowerCase(), - _inputMaterial.value, - ] - } - handleSourceNotConfigured( - configuredMaterialList, - wf, - _ciNode[MATERIAL_TYPE.inputMaterialList], - !gitMaterials[wf.ciConfiguredGitMaterialId], - ) - } - _selectedAppWorkflowList.push({ - workFlowId: wf.id, - appId: wf.appId, - name: wf.name, - ciPipelineName: _ciNode.title, - ciPipelineId: _ciNode.id, - isFirstTrigger: _ciNode.status?.toLowerCase() === BUILD_STATUS.NOT_TRIGGERED, - isCacheAvailable: _ciNode.storageConfigured, - isLinkedCI: _ciNode.isLinkedCI, - isLinkedCD: _ciNode.isLinkedCD, - title: _ciNode.title, - isWebhookCI: _ciNode.type === WorkflowNodeType.WEBHOOK, - parentAppId: _ciNode.parentAppId, - parentCIPipelineId: _ciNode.parentCiPipeline, - material: _ciNode.inputMaterialList, - warningMessage: getWarningMessage(_ciNode), - errorMessage: getErrorMessage(wf.appId, _ciNode), - hideSearchHeader: - _ciNode.type === WorkflowNodeType.WEBHOOK || _ciNode.isLinkedCI || _ciNode.isLinkedCD, - filteredCIPipelines: filteredCIPipelines.get(wf.appId), - isJobCI: !!_ciNode.isJobCI, - }) - } - } - }) - return _selectedAppWorkflowList.sort((a, b) => sortCallback('name', a, b)) - } - if (pageViewType === ViewType.LOADING) { return } @@ -1980,134 +478,19 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou ) } - const resetAbortController = () => { - abortCIBuildRef.current = new AbortController() - } - - const renderCIMaterial = (): JSX.Element | null => { - let nd: CommonNodeAttr - let _appID - if (selectedCINode?.id) { - const configuredMaterialList = new Map>() - for (const _wf of filteredWorkflows) { - nd = _wf.nodes.find((node) => +node.id == selectedCINode.id && node.type === selectedCINode.type) - if (nd) { - if (!nd[materialType]) { - nd[materialType] = [] - } - - const gitMaterials = new Map() - for (const _inputMaterial of nd.inputMaterialList) { - gitMaterials[_inputMaterial.gitMaterialId] = [ - _inputMaterial.gitMaterialName.toLowerCase(), - _inputMaterial.value, - ] - } - configuredMaterialList[_wf.name] = new Set() - _appID = _wf.appId - handleSourceNotConfigured( - configuredMaterialList, - _wf, - nd[materialType], - !gitMaterials[_wf.ciConfiguredGitMaterialId], - ) - break - } - } - } - const material = nd?.[materialType] || [] - if (selectedCINode?.id) { - return ( - - - - - - - - - ) - } - return null - } - const renderBulkCDMaterial = (): JSX.Element | null => { if (!showBulkCDModal) { return null } - const bulkCDDetailTypeResponse = createBulkCDTriggerData() - const _selectedAppWorkflowList: BulkCDDetailType[] = bulkCDDetailTypeResponse.bulkCDDetailType - - const { uniqueReleaseTags } = bulkCDDetailTypeResponse - - const feasiblePipelineIds = new Set( - getSelectedCDNodesWithArtifacts(filteredWorkflows).map(({ node }) => +node.id), - ) - - // Have to look for its each prop carefully - // No need to send uniqueReleaseTags will get those in BulkCDTrigger itself return ( - ) } @@ -2116,26 +499,14 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou if (!showBulkCIModal) { return null } - const _selectedAppWorkflowList: BulkCIDetailType[] = createBulkCITriggerData() + return ( - ) } @@ -2156,140 +527,18 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou ) } - const renderCDMaterialContent = ({ - node, - appId, - workflowId, - selectedAppName, - doesWorkflowContainsWebhook, - ciNodeId, - }: RenderCDMaterialContentProps) => { - const configurePluginURL = getCDPipelineURL( - String(appId), - workflowId, - doesWorkflowContainsWebhook ? '0' : ciNodeId, - doesWorkflowContainsWebhook, - node?.id, - true, - ) - - return ( - - ) - } - - const renderCDMaterial = (): JSX.Element | null => { - if (!selectedCDNode?.id) { - return null - } - - if (location.search.includes('cd-node') || location.search.includes('rollback-node')) { - let node: CommonNodeAttr - let _appID - let selectedAppName: string - let workflowId: string - let selectedCINode: CommonNodeAttr - - if (selectedCDNode?.id) { - for (const _wf of filteredWorkflows) { - node = _wf.nodes.find((el) => { - return +el.id == selectedCDNode.id && el.type == selectedCDNode.type - }) - if (node) { - selectedCINode = _wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - workflowId = _wf.id - _appID = _wf.appId - selectedAppName = _wf.name - break - } - } - } - const material = node?.[materialType] || [] - - return ( - -
0 ? '' : 'no-material' - }`} - onClick={stopPropagation} - > - {isCDLoading ? ( - <> -
- -
-
- -
- - ) : ( - renderCDMaterialContent({ - node, - appId: _appID, - selectedAppName, - workflowId, - doesWorkflowContainsWebhook: selectedCINode?.type === WorkflowNodeType.WEBHOOK, - ciNodeId: selectedCINode?.id, - }) - )} -
-
- ) - } - - return null - } - const renderApprovalMaterial = () => { if (ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { - let node: CommonNodeAttr - let _appID - if (selectedCDNode?.id) { - for (const _wf of filteredWorkflows) { - node = _wf.nodes.find((el) => { - return +el.id == selectedCDNode.id && el.type == selectedCDNode.type - }) - if (node) { - _appID = _wf.appId - break - } - } - } + const { node, appId } = getSelectedNodeAndAppId(filteredWorkflows, location.search) return ( { - return ( - - ( + + + + + + {showPreDeployment && ( +
+ Trigger Pre-deployment stage +
+ )} +
- - - - {showPreDeployment && ( -
- Trigger Pre-deployment stage -
- )} + Trigger Deployment +
+ {showPostDeployment && (
- Trigger Deployment + Trigger Post-deployment stage
- {showPostDeployment && ( -
- Trigger Post-deployment stage -
- )} -
-
- ) - } + )} + +
+ ) const renderBulkTriggerActionButtons = (): JSX.Element => { const selectedWorkflows = filteredWorkflows.filter((wf) => wf.isSelected) @@ -2370,23 +617,16 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou text="Build image" onClick={onShowBulkCIModal} size={ComponentSizeType.medium} - isLoading={isCILoading} />
{_showPopupMenu && renderDeployPopupMenu()}
@@ -2394,32 +634,30 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou ) } - const renderSelectedApps = (): JSX.Element => { - return ( -
- -
- -
-
-
-
- {selectedAppList.length} application{selectedAppList.length > 1 ? 's' : ''} selected -
-
- {sortObjectArrayAlphabetically(selectedAppList, 'name').map((app, index) => ( - - {app['name']} - {index !== selectedAppList.length - 1 && , } - - ))} -
+ const renderSelectedApps = (): JSX.Element => ( +
+ +
+ +
+
+
+
+ {selectedAppList.length} application{selectedAppList.length > 1 ? 's' : ''} selected +
+
+ {sortObjectArrayAlphabetically(selectedAppList, 'name').map((app, index) => ( + + {app.name} + {index !== selectedAppList.length - 1 && , } + + ))}
- ) - } +
+ ) - const handleModalClose = () => { + const revertToPreviousURL = () => { history.push(match.url) } @@ -2433,6 +671,10 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou setSelectedWebhookNode(null) } + const openCIMaterialModal = (ciNodeId: string) => { + history.push(`${match.url}${URLS.BUILD}/${ciNodeId}`) + } + const renderWebhookAddImageModal = () => { if ( WebhookAddImageModal && @@ -2448,37 +690,35 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou return null } - const renderWorkflow = (): JSX.Element => { - return ( - <> - {filteredWorkflows.map((workflow, index) => { - return ( - - ) - })} - - {renderWebhookAddImageModal()} - - ) - } + const renderWorkflow = (): JSX.Element => ( + <> + {filteredWorkflows.map((workflow, index) => ( + + ))} + + {renderWebhookAddImageModal()} + + ) return (
@@ -2497,32 +737,31 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou - - {renderWorkflow()} - {renderCIMaterial()} - {renderCDMaterial()} - {renderBulkCDMaterial()} - {renderBulkCIMaterial()} - {renderApprovalMaterial()} - {renderBulkSourceChange()} - + {renderWorkflow()} + + + + + + + + {renderBulkCDMaterial()} + {renderBulkCIMaterial()} + {renderApprovalMaterial()} + {renderBulkSourceChange()}
{!!selectedAppList.length && ( @@ -2534,3 +773,5 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou
) } + +export default EnvTriggerView diff --git a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx index 93c1a0d328..fcb40f8a72 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx @@ -33,10 +33,8 @@ export const TriggerResponseModalFooter = ({ closePopup, isLoading, responseList, - skipHibernatedApps, onClickRetryBuild, onClickRetryDeploy, - pipelineIdVsStrategyMap, }: TriggerResponseModalFooterProps) => { const isShowRetryButton = responseList?.some((response) => response.status === BulkResponseStatus.FAIL) @@ -52,7 +50,7 @@ export const TriggerResponseModalFooter = ({ if (onClickRetryBuild) { onClickRetryBuild(appsToRetry) } else { - onClickRetryDeploy(skipHibernatedApps, pipelineIdVsStrategyMap, appsToRetry) + onClickRetryDeploy(appsToRetry) } } @@ -83,7 +81,7 @@ const TriggerResponseModalBody = ({ responseList, isLoading, isVirtualEnv }: Tri return } return ( -
+
- app.isLinkedCI || app.isWebhookCI || app.isLinkedCD - -export const getIsNonApprovedImageSelected = (appList: BulkCDDetailType[]): boolean => - appList.some((app) => { - if (!app.isExceptionUser) { - return false - } - - return (app.material || []).some( - (material) => material.isSelected && !getIsMaterialApproved(material.userApprovalMetadata), - ) - }) - -export const getIsImageApprovedByDeployerSelected = (appList: BulkCDDetailType[]): boolean => - appList.some((app) => { - if (!app.isExceptionUser) { - return false - } - - return (app.material || []).some( - (material) => - material.isSelected && - !material.canApproverDeploy && - material.userApprovalMetadata?.hasCurrentUserApproved, - ) - }) +import { BulkCDDetailType } from '../../AppGroup.types' export const getSelectedCDNode = (bulkTriggerType: DeploymentNodeType, _cdNode: CommonNodeAttr) => { if (bulkTriggerType === DeploymentNodeType.PRECD) { @@ -61,7 +34,41 @@ export const getSelectedCDNode = (bulkTriggerType: DeploymentNodeType, _cdNode: return null } -export const getSelectedAppListForBulkStrategy = (appList: BulkCDDetailType[], feasiblePipelineIds: Set) => - appList - .map((app) => ({ pipelineId: +app.cdPipelineId, appName: app.name })) - .filter(({ pipelineId }) => feasiblePipelineIds.has(pipelineId)) +export const getSelectedAppListForBulkStrategy = ( + appInfoRes: DeployImageContentProps['appInfoMap'], +): Pick[] => { + const feasiblePipelineIds: Set = Object.values(appInfoRes).reduce((acc, appDetails) => { + const materials = appDetails.materialResponse?.materials || [] + const isMaterialSelected = materials.some((material) => material.isSelected) + + if (isMaterialSelected) { + acc.add(+appDetails.pipelineId) + } + + return acc + }, new Set()) + + const appList: Pick[] = Object.values(appInfoRes).map((appDetails) => ({ + pipelineId: +appDetails.pipelineId, + appName: appDetails.appName, + })) + + return appList.filter(({ pipelineId }) => feasiblePipelineIds.has(pipelineId)) +} + +export const getSelectedNodeAndAppId = ( + workflows: WorkflowType[], + search: string, +): { node: CommonNodeAttr; appId: number } => { + const { cdNodeId, nodeType } = getNodeIdAndTypeFromSearch(search) + + const result = workflows.reduce( + (acc, workflow) => { + if (acc.node) return acc + const node = workflow.nodes.find((n) => n.id === cdNodeId && n.type === nodeType) + return node ? { node, appId: workflow.appId } : acc + }, + { node: undefined, appId: undefined }, + ) + return result +} diff --git a/src/components/CIPipelineN/EnvironmentList.tsx b/src/components/CIPipelineN/EnvironmentList.tsx index ea872f8209..7e276b9b99 100644 --- a/src/components/CIPipelineN/EnvironmentList.tsx +++ b/src/components/CIPipelineN/EnvironmentList.tsx @@ -82,6 +82,10 @@ export const EnvironmentList = ({ return _selectedEnv } + const getIsOptionSelected = (option: EnvironmentWithSelectPickerType): boolean => { + return selectedEnv?.id === option.id + } + const getEnvironmentSelectLabel = (): JSX.Element => { if (isBuildStage) { return Execute tasks in environment @@ -107,6 +111,7 @@ export const EnvironmentList = ({ onChange={selectEnvironment} size={ComponentSizeType.large} variant={isBorderLess ? SelectPickerVariantType.COMPACT : SelectPickerVariantType.DEFAULT} + isOptionSelected={getIsOptionSelected} />
diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx index c6d5e44045..b213c0a2ec 100644 --- a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -31,6 +31,7 @@ import { Textarea, TextareaProps, Tooltip, + UsePopoverReturnType, validateRequiredPositiveNumber, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' @@ -174,10 +175,10 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay } // RENDERERS - const renderContent = () => { + const renderContent = (scrollableRef: UsePopoverReturnType['scrollableRef']) => { if (isFormatFile) { return ( -
+
+
@@ -242,7 +243,7 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay if (choices.length) { return ( -
+
+ } + > +
+ Allow Custom input +
+ + + )} + {AskValueAtRuntimeCheckbox && ( + + )} +
+ )} + + )} ) } diff --git a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx index 868d1561b9..ca2d24b075 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx @@ -60,51 +60,53 @@ export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOver return ( - <> -
- -