Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@ export default function AzureProfilePage() {
setLoggingOut(true);
try {
// Import dynamically to avoid circular dependencies
const { runCommandAsync } = await import('../../utils/azure/az-cli');
await runCommandAsync('az', ['logout']);
const { runAzCommand } = await import('../../utils/azure/az-cli');
const result = await runAzCommand(['logout']);

if (result.stderr && (result.stderr.includes('ERROR') || result.stderr.includes('error'))) {
console.error('Azure CLI logout error:', result.stderr);
setLoggingOut(false);
return;
}

// Trigger update event for sidebar label
window.dispatchEvent(new CustomEvent('azure-auth-update'));
Expand Down
105 changes: 91 additions & 14 deletions plugins/aks-desktop/src/components/Deploy/DeployButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// Licensed under the Apache 2.0.

import { Icon } from '@iconify/react';
import { Button, Dialog } from '@mui/material';
import React, { useEffect } from 'react';
import { Box, Button, Dialog } from '@mui/material';
import React, { useCallback, useEffect, useState } from 'react';
import { useAzureAuth } from '../../hooks/useAzureAuth';
import type { GitHubRepo } from '../../types/github';
import { getClusterInfo } from '../../utils/azure/az-cli';
import DeployWizard from '../DeployWizard/DeployWizard';
import { getActivePipeline } from '../GitHubPipeline/hooks/useGitHubPipelineOrchestration';
import { useDeployUrlParams } from './hooks/useDeployUrlParams';
import { useDialogState } from './hooks/useDialogState';

Expand Down Expand Up @@ -39,6 +43,48 @@ interface DeployButtonProps {
function DeployButton({ project }: DeployButtonProps) {
const urlParams = useDeployUrlParams();
const dialogState = useDialogState();
const azureAuth = useAzureAuth();
const [azureContext, setAzureContext] = useState<{
subscriptionId: string;
resourceGroup: string;
tenantId: string;
} | null>(null);
const [activePipelineRepo, setActivePipelineRepo] = useState<GitHubRepo | null>(null);
const [resumeRepo, setResumeRepo] = useState<GitHubRepo | undefined>(undefined);

// Resolve Azure context from cluster info
useEffect(() => {
const cluster = project.clusters?.[0];
if (!cluster || !azureAuth.isLoggedIn) return;
(async () => {
try {
const clusterInfo = await getClusterInfo(cluster);
setAzureContext({
subscriptionId: clusterInfo.subscriptionId ?? '',
resourceGroup: clusterInfo.resourceGroup ?? '',
tenantId: azureAuth.tenantId ?? '',
});
} catch (error) {
console.error('Failed to resolve Azure context:', error);
}
})();
}, [project.clusters, azureAuth.isLoggedIn, azureAuth.tenantId]);

// Check for an in-progress pipeline on mount and when the dialog closes
const checkActivePipeline = useCallback(() => {
const cluster = project.clusters?.[0];
const ns = project.namespaces?.[0];
if (!cluster || !ns) {
setActivePipelineRepo(null);
return;
}
const active = getActivePipeline(cluster, ns);
setActivePipelineRepo(active?.repo ?? null);
}, [project.clusters, project.namespaces]);

useEffect(() => {
checkActivePipeline();
}, [checkActivePipeline]);

// Open dialog when URL parameters indicate we should
useEffect(() => {
Expand All @@ -54,27 +100,56 @@ function DeployButton({ project }: DeployButtonProps) {
]);

const handleClickOpen = () => {
setResumeRepo(undefined);
dialogState.openDialog();
};

const handleResumeClick = () => {
if (activePipelineRepo) {
setResumeRepo(activePipelineRepo);
dialogState.openDialog();
}
};

const handleClose = () => {
dialogState.closeDialog();
setResumeRepo(undefined);
// Re-check active pipeline in case state changed while dialog was open
checkActivePipeline();
};

return (
<>
<Button
variant="contained"
color="primary"
startIcon={<Icon icon="mdi:cloud-upload" />}
onClick={handleClickOpen}
sx={{
textTransform: 'none',
fontWeight: 'bold',
}}
>
Deploy Application
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="contained"
color="primary"
startIcon={<Icon icon="mdi:cloud-upload" />}
onClick={handleClickOpen}
sx={{
textTransform: 'none',
fontWeight: 'bold',
}}
>
Deploy Application
</Button>
{activePipelineRepo && (
<Button
variant="outlined"
color="info"
size="small"
startIcon={<Icon icon="mdi:progress-clock" />}
onClick={handleResumeClick}
sx={{
textTransform: 'none',
fontWeight: 500,
fontSize: '0.8rem',
}}
>
Pipeline in progress
</Button>
)}
</Box>
<Dialog
open={dialogState.open}
onClose={handleClose}
Expand All @@ -91,7 +166,9 @@ function DeployButton({ project }: DeployButtonProps) {
cluster={project.clusters?.[0] || undefined}
namespace={project.namespaces?.[0] || undefined}
initialApplicationName={dialogState.initialApplicationName}
azureContext={azureContext ?? undefined}
onClose={handleClose}
resumePipelineRepo={resumeRepo}
/>
</Dialog>
</>
Expand Down
152 changes: 100 additions & 52 deletions plugins/aks-desktop/src/components/DeployWizard/DeployWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
} from '@mui/material';
import React, { useEffect, useState } from 'react';
import YAML from 'yaml';
import type { GitHubRepo } from '../../types/github';
import { Breadcrumb } from '../CreateAKSProject/components/Breadcrumb';
import GitHubPipelineWizard from '../GitHubPipeline/GitHubPipelineWizard';
import ConfigureContainer from './components/ConfigureContainer';
import ConfigureYAML from './components/ConfigureYAML';
import Deploy from './components/Deploy';
Expand All @@ -26,7 +28,10 @@ type DeployWizardProps = {
cluster?: string;
namespace?: string;
initialApplicationName?: string;
azureContext?: { subscriptionId: string; resourceGroup: string; tenantId: string };
onClose?: () => void;
/** Pre-selected repo to resume an in-progress pipeline. */
resumePipelineRepo?: GitHubRepo;
};

enum WizardStep {
Expand All @@ -37,14 +42,25 @@ enum WizardStep {

const STEP_NAMES: string[] = ['Source', 'Configure', 'Deploy'];

const FEATURE_FLAG_GITHUB_PIPELINE = 'aks-desktop:feature:github-pipeline';
const isGitHubPipelineEnabled = (): boolean =>
localStorage.getItem(FEATURE_FLAG_GITHUB_PIPELINE) === 'true';

export default function DeployWizard({
cluster,
namespace,
initialApplicationName,
azureContext,
onClose,
resumePipelineRepo,
}: DeployWizardProps) {
const [activeStep, setActiveStep] = useState(WizardStep.SOURCE);
const [sourceType, setSourceType] = useState<null | 'yaml' | 'container'>(null);
const [activeStep, setActiveStep] = useState(
resumePipelineRepo ? WizardStep.DEPLOY : WizardStep.SOURCE
);
const [sourceType, setSourceType] = useState<null | 'yaml' | 'container'>(
resumePipelineRepo ? 'container' : null
);
const [showPipeline, setShowPipeline] = useState(!!resumePipelineRepo);
const [yamlEditorValue, setYamlEditorValue] = useState<string>('');
const [yamlError, setYamlError] = useState<string | null>(null);
const [deploying, setDeploying] = useState(false);
Expand All @@ -55,15 +71,23 @@ export default function DeployWizard({
// Container configuration state
const containerConfig = useContainerConfiguration(initialApplicationName);

// Generate container preview YAML when entering the Deploy step.
// Uses a ref to compare against the previous preview to avoid infinite re-renders
// (setConfig creates a new object, which would re-trigger this effect).
const prevContainerPreviewRef = React.useRef<string>('');
useEffect(() => {
if (activeStep === WizardStep.DEPLOY && sourceType === 'container') {
containerConfig.setConfig(prev => ({
...prev,
containerPreviewYaml: generateYamlForContainer({
const preview = generateYamlForContainer({
...containerConfig.config,
namespace,
});
if (preview !== prevContainerPreviewRef.current) {
prevContainerPreviewRef.current = preview;
containerConfig.setConfig(prev => ({
...prev,
namespace,
}),
}));
containerPreviewYaml: preview,
}));
}
}
// Generate preview with namespace override for user YAML in the Review step
if (activeStep === WizardStep.DEPLOY && sourceType === 'yaml') {
Expand Down Expand Up @@ -191,6 +215,25 @@ export default function DeployWizard({
</Box>
);
case WizardStep.DEPLOY:
if (showPipeline) {
return !azureContext ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : (
<GitHubPipelineWizard
clusterName={cluster || ''}
namespace={namespace || ''}
appName={containerConfig.config.appName || initialApplicationName || ''}
subscriptionId={azureContext.subscriptionId}
resourceGroup={azureContext.resourceGroup}
tenantId={azureContext.tenantId}
onClose={() => setShowPipeline(false)}
initialRepo={resumePipelineRepo}
containerConfig={containerConfig.config}
/>
);
}
return (
<Deploy
sourceType={sourceType}
Expand All @@ -200,6 +243,8 @@ export default function DeployWizard({
containerPreviewYaml={containerConfig.config.containerPreviewYaml}
deployResult={deployResult}
deployMessage={deployMessage}
showGitHubPipeline={isGitHubPipelineEnabled() && sourceType === 'container'}
onSetupGitHubPipeline={() => setShowPipeline(true)}
/>
);
default:
Expand Down Expand Up @@ -229,54 +274,57 @@ export default function DeployWizard({
>
{renderStepContent()}
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 3,
py: 3,
borderTop: '1px solid',
borderColor: 'divider',
marginTop: 2,
}}
>
{activeStep === WizardStep.DEPLOY ? (
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
{deployResult ? (
<Button variant="contained" onClick={onClose}>
Close
</Button>
) : (
{/* Hide button bar when GitHub Pipeline wizard is active — it has its own navigation */}
{showPipeline && activeStep === WizardStep.DEPLOY ? null : (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 3,
py: 3,
borderTop: '1px solid',
borderColor: 'divider',
marginTop: 2,
}}
>
{activeStep === WizardStep.DEPLOY ? (
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
{deployResult ? (
<Button variant="contained" onClick={onClose}>
Close
</Button>
) : (
<Button
variant="contained"
onClick={handleDeploy}
disabled={deploying}
startIcon={deploying ? <CircularProgress size={20} /> : null}
>
{deploying ? 'Deploying...' : 'Deploy'}
</Button>
)}
</Box>
) : (
<>
<Box>
{activeStep > WizardStep.SOURCE && (
<Button variant="outlined" onClick={handleBack}>
Back
</Button>
)}
</Box>
<Button
variant="contained"
onClick={handleDeploy}
disabled={deploying}
startIcon={deploying ? <CircularProgress size={20} /> : null}
onClick={handleNext}
disabled={!isStepValid(activeStep)}
>
{deploying ? 'Deploying...' : 'Deploy'}
Next
</Button>
)}
</Box>
) : (
<>
<Box>
{activeStep > WizardStep.SOURCE && (
<Button variant="outlined" onClick={handleBack}>
Back
</Button>
)}
</Box>
<Button
variant="contained"
onClick={handleNext}
disabled={!isStepValid(activeStep)}
>
Next
</Button>
</>
)}
</Box>
</>
)}
</Box>
)}
</CardContent>
</Card>
</Container>
Expand Down
Loading