diff --git a/ui/src/features/stage/promotion-details-modal.tsx b/ui/src/features/stage/promotion-details-modal.tsx index 706003867..e86c56c8f 100644 --- a/ui/src/features/stage/promotion-details-modal.tsx +++ b/ui/src/features/stage/promotion-details-modal.tsx @@ -106,7 +106,13 @@ const Step = ({ }; const UiPlugins = uiPlugins - .filter((plugin) => plugin.DeepLinkPlugin?.PromotionStep?.shouldRender({ step, result })) + .filter((plugin) => + plugin.DeepLinkPlugin?.PromotionStep?.shouldRender({ + step, + result, + output: output as Record + }) + ) .map((plugin) => plugin.DeepLinkPlugin?.PromotionStep?.render); return { diff --git a/ui/src/features/stage/promotions.tsx b/ui/src/features/stage/promotions.tsx index 0f53565d8..9010fb21a 100644 --- a/ui/src/features/stage/promotions.tsx +++ b/ui/src/features/stage/promotions.tsx @@ -19,7 +19,7 @@ import { listPromotions } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery'; import { KargoService } from '@ui/gen/service/v1alpha1/service_connect'; -import { ListPromotionsResponse } from '@ui/gen/service/v1alpha1/service_pb'; +import { ArgoCDShard, ListPromotionsResponse } from '@ui/gen/service/v1alpha1/service_pb'; import { Freight, Promotion } from '@ui/gen/v1alpha1/generated_pb'; import uiPlugins from '@ui/plugins'; import { UiPluginHoles } from '@ui/plugins/ui-plugin-hole/ui-plugin-holes'; @@ -27,7 +27,7 @@ import { UiPluginHoles } from '@ui/plugins/ui-plugin-hole/ui-plugin-holes'; import { PromotionDetailsModal } from './promotion-details-modal'; import { hasAbortRequest, promotionCompareFn } from './utils/promotion'; -export const Promotions = () => { +export const Promotions = ({ argocdShard }: { argocdShard?: ArgoCDShard }) => { const client = useQueryClient(); const { name: projectName, stageName } = useParams(); @@ -176,16 +176,29 @@ export const Promotions = () => { }, { title: 'Extra', - render: (_, promotion) => { + render: (_, promotion, promotionIndex) => { const UiPlugins = uiPlugins - .filter((plugin) => plugin.DeepLinkPlugin?.Promotion?.shouldRender({ promotion })) + .filter((plugin) => + plugin.DeepLinkPlugin?.Promotion?.shouldRender({ + promotion, + isLatestPromotion: promotionIndex === 0 + }) + ) .map((plugin) => plugin.DeepLinkPlugin?.Promotion?.render); if (UiPlugins?.length > 0) { return ( {UiPlugins.map( - (ApplyPlugin, idx) => ApplyPlugin && + (ApplyPlugin, idx) => + ApplyPlugin && ( + + ) )} ); diff --git a/ui/src/features/stage/stage-actions.tsx b/ui/src/features/stage/stage-actions.tsx index 92f6e20ef..372394de1 100644 --- a/ui/src/features/stage/stage-actions.tsx +++ b/ui/src/features/stage/stage-actions.tsx @@ -1,8 +1,6 @@ -import { createConnectQueryKey, useMutation, useQuery } from '@connectrpc/connect-query'; +import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query'; import { - faChevronDown, faExclamationCircle, - faExternalLinkAlt, faPen, faRedo, faRefresh, @@ -10,7 +8,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useQueryClient } from '@tanstack/react-query'; -import { Button, Dropdown, Space, Tooltip } from 'antd'; +import { Button, Space } from 'antd'; import React from 'react'; import { generatePath, useNavigate, useParams } from 'react-router-dom'; @@ -18,7 +16,6 @@ import { paths } from '@ui/config/paths'; import { abortVerification, deleteStage, - getConfig, queryFreight, refreshStage, reverify @@ -28,7 +25,6 @@ import { Stage } from '@ui/gen/v1alpha1/generated_pb'; import { useConfirmModal } from '../common/confirm-modal/use-confirm-modal'; import { useModal } from '../common/modal/use-modal'; import { currentFreightHasVerification } from '../common/utils'; -import { getPromotionArgoCDApps } from '../promotion-directives/utils'; import { EditStageModal } from './edit-stage-modal'; @@ -83,23 +79,6 @@ export const StageActions = ({ } }, [stage, shouldRefetchFreights]); - const { data: config } = useQuery(getConfig); - const argoCDAppsLinks = React.useMemo(() => { - const shardKey = stage?.metadata?.labels['kargo.akuity.io/shard'] || ''; - const shard = config?.argocdShards?.[shardKey]; - - const argocdApps = getPromotionArgoCDApps(stage); - - if (!shard || !argocdApps.length) { - return []; - } - - return argocdApps.map((appName) => ({ - label: appName, - url: `${shard.url}/applications/${shard.namespace}/${appName}` - })); - }, [config, stage]); - const { mutate: reverifyStage, isPending } = useMutation(reverify); const { mutate: abortVerificationAction } = useMutation(abortVerification); @@ -107,38 +86,6 @@ export const StageActions = ({ return ( - {argoCDAppsLinks.length === 1 && ( - - - - )} - {argoCDAppsLinks.length > 1 && ( - ({ - label: ( - - - {item?.label} - - ), - key: i - })) - }} - trigger={['click']} - > - - - )} {currentFreightHasVerification(stage) && ( <> {verificationEnabled && ( diff --git a/ui/src/features/stage/stage-details.tsx b/ui/src/features/stage/stage-details.tsx index 2986b702d..f9f09c7a6 100644 --- a/ui/src/features/stage/stage-details.tsx +++ b/ui/src/features/stage/stage-details.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@connectrpc/connect-query'; import { Divider, Drawer, Tabs, Typography } from 'antd'; import moment from 'moment'; import { useMemo, useState } from 'react'; @@ -5,6 +6,7 @@ import { generatePath, useNavigate, useParams } from 'react-router-dom'; import { paths } from '@ui/config/paths'; import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon'; +import { getConfig } from '@ui/gen/service/v1alpha1/service-KargoService_connectquery'; import { Stage, VerificationInfo } from '@ui/gen/v1alpha1/generated_pb'; import { Description } from '../common/description'; @@ -42,6 +44,11 @@ export const StageDetails = ({ stage }: { stage: Stage }) => { .sort((a, b) => moment(b.startTime?.toDate()).diff(moment(a.startTime?.toDate()))); }, [stage]); + const { data: config } = useQuery(getConfig); + + const shardKey = stage?.metadata?.labels['kargo.akuity.io/shard'] || ''; + const argocdShard = config?.argocdShards?.[shardKey]; + return ( {stage && ( @@ -79,7 +86,7 @@ export const StageDetails = ({ stage }: { stage: Stage }) => { { key: '1', label: 'Promotions', - children: + children: }, { key: '2', diff --git a/ui/src/plugins/argocd-plugin.tsx b/ui/src/plugins/argocd-plugin.tsx new file mode 100644 index 000000000..a6769fa56 --- /dev/null +++ b/ui/src/plugins/argocd-plugin.tsx @@ -0,0 +1,95 @@ +import { faChevronDown } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Dropdown, Space, Tooltip } from 'antd'; + +import { getPromotionHealthCheckConfig } from './plugin-helper'; +import { PluginsInstallation } from './plugin-interfaces'; + +const argocdPlugin: PluginsInstallation = { + DeepLinkPlugin: { + Promotion: { + shouldRender(opts) { + return ( + Boolean(opts?.isLatestPromotion) && + Boolean(opts.promotion?.spec?.steps?.find((step) => step?.uses === 'argocd-update')) && + (opts.promotion?.status?.healthChecks?.length || 0) > 0 + ); + }, + render(props) { + if (!props.unstable_argocdShardUrl) { + return ( + + ArgoCD + + ); + } + + // argocd shards sometimes might have base path included + // in those cases, we must not omit those pathname in order to have valid + const unstable_argocdShardUrl = props.unstable_argocdShardUrl.endsWith('/') + ? props.unstable_argocdShardUrl.slice(0, -1) + : props.unstable_argocdShardUrl; + + const healthChecks = (props.promotion?.status?.healthChecks || []).filter( + (hc) => hc?.uses === 'argocd-update' + ); + + // health checks contains nested apps + // ie. healthChecks: + // - uses: argocd-update + // config: + // apps: + // - name: app + // namespace: ns + const apps = []; + + for (const healthCheck of healthChecks) { + const healthCheckConfig = getPromotionHealthCheckConfig(healthCheck); + + // @ts-expect-error we don't have type but as long as we are sure whats coming in.. its safe to assume + for (const app of healthCheckConfig?.apps || []) { + apps.push(app); + } + } + + if (apps.length === 1) { + return ( + + ArgoCD + + ); + } + + return ( + ({ + key: idx, + label: ( + + {app.namespace} - {app.name} + + ) + })) + }} + > + e.preventDefault()}> + + ArgoCD + + + + + ); + } + } + } +}; + +export default argocdPlugin; diff --git a/ui/src/plugins/index.tsx b/ui/src/plugins/index.tsx index 037a74fd2..b782f37a3 100644 --- a/ui/src/plugins/index.tsx +++ b/ui/src/plugins/index.tsx @@ -1,6 +1,7 @@ +import argocdPlugin from './argocd-plugin'; import { PluginsInstallation } from './plugin-interfaces'; import prPlugin from './pr-plugin'; -const plugins: PluginsInstallation[] = [prPlugin]; +const plugins: PluginsInstallation[] = [prPlugin, argocdPlugin]; export default plugins; diff --git a/ui/src/plugins/plugin-helper.ts b/ui/src/plugins/plugin-helper.ts index 4d9524258..af7c17919 100644 --- a/ui/src/plugins/plugin-helper.ts +++ b/ui/src/plugins/plugin-helper.ts @@ -1,4 +1,4 @@ -import { Promotion, PromotionStep } from '@ui/gen/v1alpha1/generated_pb'; +import { HealthCheckStep, Promotion, PromotionStep } from '@ui/gen/v1alpha1/generated_pb'; import { decodeRawData } from '@ui/utils/decode-raw-data'; export const getPromotionStepConfig = (step: PromotionStep): Record => @@ -21,5 +21,15 @@ export const getPromotionState = (promotion: Promotion): Record => + JSON.parse( + decodeRawData({ + result: { + case: 'raw', + value: hc?.config?.raw || new Uint8Array() + } + }) + ); + export const getPromotionStepAlias = (promotionStep: PromotionStep, stepIndex: string) => promotionStep?.as || `step-${stepIndex}`; diff --git a/ui/src/plugins/plugin-interfaces.ts b/ui/src/plugins/plugin-interfaces.ts index 57357456f..e6cff5f78 100644 --- a/ui/src/plugins/plugin-interfaces.ts +++ b/ui/src/plugins/plugin-interfaces.ts @@ -14,27 +14,40 @@ import { ReactNode } from 'react'; import { PromotionDirectiveStepStatus } from '@ui/features/common/promotion-directive-step-status/utils'; import { Promotion, PromotionStep } from '@ui/gen/v1alpha1/generated_pb'; +interface DeepLinksPluginProps { + PromotionStep: { + // step metadata + step: PromotionStep; + // flexible to render based on status + result: PromotionDirectiveStepStatus; + // output from steps + output?: Record; + }; + + Promotion: { + promotion: Promotion; + + // which argocd shard does this promotion affect + unstable_argocdShardUrl?: string; + + isLatestPromotion?: boolean; + }; +} + export interface DeepLinkPluginsInstallation { // Scopes // Plugin generates information from given plugin step and renders in the step view in Promotion steps modal PromotionStep?: { // thoughts.. instead of coupling this to name of step, this would open doors for plugin development based on combination of promotion steps - shouldRender: (opts: { step: PromotionStep; result: PromotionDirectiveStepStatus }) => boolean; - render: (props: { - // step metadata - step: PromotionStep; - // flexible to render based on status - result: PromotionDirectiveStepStatus; - // output from steps - output?: Record; - }) => ReactNode; + shouldRender: (opts: DeepLinksPluginProps['PromotionStep']) => boolean; + render: (props: DeepLinksPluginProps['PromotionStep']) => ReactNode; }; // Plugin summarises promotion and provide useful link(s) and renders in the promotion list view Promotion?: { - shouldRender: (opts: { promotion: Promotion }) => boolean; - render: (props: { promotion: Promotion }) => ReactNode; + shouldRender: (opts: DeepLinksPluginProps['Promotion']) => boolean; + render: (props: DeepLinksPluginProps['Promotion']) => ReactNode; }; } diff --git a/ui/src/plugins/pr-plugin.tsx b/ui/src/plugins/pr-plugin.tsx index c976dcc16..fa17ddf72 100644 --- a/ui/src/plugins/pr-plugin.tsx +++ b/ui/src/plugins/pr-plugin.tsx @@ -49,7 +49,7 @@ const getPullRequestLink = ( }; // TODO: refactor as it is getting bigger -const plugin: PluginsInstallation = { +const prPlugin: PluginsInstallation = { DeepLinkPlugin: { PromotionStep: { shouldRender({ step, result }) { @@ -153,4 +153,4 @@ const plugin: PluginsInstallation = { } }; -export default plugin; +export default prPlugin; diff --git a/ui/src/plugins/ui-plugin-hole/deep-link-promotion-step.tsx b/ui/src/plugins/ui-plugin-hole/deep-link-promotion-step.tsx index 8bccc4e54..6d113a6e9 100644 --- a/ui/src/plugins/ui-plugin-hole/deep-link-promotion-step.tsx +++ b/ui/src/plugins/ui-plugin-hole/deep-link-promotion-step.tsx @@ -10,7 +10,10 @@ export const DeepLinkPromotionStep = ({ return (
{ // prevent opening the collapsible menu e.stopPropagation(); diff --git a/ui/src/plugins/ui-plugin-hole/deep-link-promotion.tsx b/ui/src/plugins/ui-plugin-hole/deep-link-promotion.tsx index f8681d68c..78f205683 100644 --- a/ui/src/plugins/ui-plugin-hole/deep-link-promotion.tsx +++ b/ui/src/plugins/ui-plugin-hole/deep-link-promotion.tsx @@ -8,7 +8,7 @@ export const DeepLinkPromotion = ({ className }: PropsWithChildren<{ className?: string }>) => ( -
+
{children}