Skip to content

Commit 4020b86

Browse files
committed
feat(devops-copilot): Implement troubleshooting with AI in the UI
1 parent bc67a5a commit 4020b86

20 files changed

Lines changed: 573 additions & 242 deletions

File tree

apps/console/src/app/app.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { GTMProvider } from '@elgorditosalsero/react-gtm-hook'
33
import * as Sentry from '@sentry/react'
44
import axios from 'axios'
55
import posthog from 'posthog-js'
6-
import { useCallback, useEffect, useState } from 'react'
6+
import { useCallback, useEffect, useRef, useState } from 'react'
77
import {
88
Navigate,
99
Route,
@@ -39,6 +39,7 @@ export function App() {
3939
const { redirectToAcceptPageGuard, onSearchUpdate, checkTokenInStorage } = useInviteMember()
4040
const [assistantOpen, setAssistantOpen] = useState(false)
4141
const [devopsCopilotOpen, setDevopsCopilotOpen] = useState(false)
42+
const sendMessageRef = useRef<((message: string, createNewChat?: boolean) => void) | null>(null)
4243

4344
useEffect(() => {
4445
onSearchUpdate()
@@ -112,7 +113,13 @@ export function App() {
112113

113114
return (
114115
<GTMProvider state={gtmParams}>
115-
<DevopsCopilotContext.Provider value={{ devopsCopilotOpen, setDevopsCopilotOpen }}>
116+
<DevopsCopilotContext.Provider
117+
value={{
118+
devopsCopilotOpen,
119+
setDevopsCopilotOpen,
120+
sendMessageRef,
121+
}}
122+
>
116123
<AssistantContext.Provider value={{ assistantOpen, setAssistantOpen }}>
117124
<ScrollToTop />
118125
<Routes>

libs/domains/environment-logs/feature/src/lib/environment-stages/environment-stages.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface EnvironmentStagesProps extends PropsWithChildren {
2121
deploymentStages?: DeploymentStageWithServicesStatuses[]
2222
preCheckStage?: EnvironmentStatusesWithStagesPreCheckStage
2323
deploymentHistory?: DeploymentHistoryEnvironmentV2
24+
banner?: React.ReactNode
2425
}
2526

2627
export function EnvironmentStages({
@@ -31,6 +32,7 @@ export function EnvironmentStages({
3132
preCheckStage,
3233
hideSkipped,
3334
setHideSkipped,
35+
banner,
3436
children,
3537
}: EnvironmentStagesProps) {
3638
const executionId = environmentStatus.last_deployment_id
@@ -56,6 +58,7 @@ export function EnvironmentStages({
5658
</HeaderEnvironmentStages>
5759
<div className="flex h-[calc(100vh-120px)] justify-center border border-t-0 border-neutral-500 bg-neutral-600">
5860
<div className="h-full w-full">
61+
{banner}
5962
<div className="flex h-full gap-0.5 overflow-y-scroll py-6 pl-4 pr-3">
6063
{!deploymentStages ? (
6164
<div className="mt-6 flex h-full w-full justify-center">

libs/domains/environments/feature/src/lib/environment-deployment-list/dropdown-services/dropdown-services.tsx

Lines changed: 94 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
type QueuedDeploymentRequestWithStages,
99
type QueuedDeploymentRequestWithStagesStagesInner,
1010
} from 'qovery-typescript-axios'
11-
import { useState } from 'react'
11+
import { useContext, useState } from 'react'
1212
import { Link, useLocation } from 'react-router-dom'
1313
import { P, match } from 'ts-pattern'
1414
import { type AnyService } from '@qovery/domains/services/data-access'
1515
import { ServiceAvatar } from '@qovery/domains/services/feature'
16+
import { DevopsCopilotContext } from '@qovery/shared/devops-copilot/context'
1617
import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL, ENVIRONMENT_STAGES_URL } from '@qovery/shared/routes'
1718
import { Indicator, StageStatusChip, StatusChip, Tooltip, TriggerActionIcon, Truncate } from '@qovery/shared/ui'
1819
import { Icon } from '@qovery/shared/ui'
@@ -36,6 +37,7 @@ const MAX_VISIBLE_STAGES = 4
3637
// https://github.com/radix-ui/primitives/issues/1294
3738
export function DropdownServices({ environment, deploymentHistory, stages }: DropdownServicesProps) {
3839
const { pathname } = useLocation()
40+
const { setDevopsCopilotOpen, sendMessageRef } = useContext(DevopsCopilotContext)
3941
const [open, setOpen] = useState(false)
4042
const [currentIndex, setCurrentIndex] = useState<number | undefined>()
4143
const [direction, setDirection] = useState(0)
@@ -235,9 +237,60 @@ export function DropdownServices({ environment, deploymentHistory, stages }: Dro
235237
</div>
236238
</div>
237239
{match(stage)
238-
.with(P.when(isDeploymentStageQueue), (s) =>
239-
s.services.map((service, index) => {
240-
return (
240+
.with(P.when(isDeploymentStageQueue), (s) => (
241+
<>
242+
{s.services.map((service, index) => {
243+
return (
244+
<DropdownMenu.Item
245+
key={index}
246+
className="flex h-[50px] w-full items-center gap-2 border-t border-neutral-200 pl-2 pr-3 text-xs text-neutral-400 transition-colors hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-none"
247+
asChild
248+
>
249+
<Link
250+
to={
251+
ENVIRONMENT_LOGS_URL(
252+
environment.organization.id,
253+
environment.project.id,
254+
environment.id
255+
) + ENVIRONMENT_STAGES_URL()
256+
}
257+
state={{ prevUrl: pathname }}
258+
>
259+
{service.details && (
260+
<ServiceAvatar
261+
border="solid"
262+
size="sm"
263+
service={
264+
'job_type' in service.details
265+
? {
266+
icon_uri: service.icon_uri ?? '',
267+
serviceType: 'JOB' as const,
268+
job_type: service.details.job_type as 'CRON' | 'LIFECYCLE',
269+
}
270+
: {
271+
icon_uri: service.icon_uri ?? '',
272+
serviceType: service.identifier.service_type as Exclude<
273+
AnyService['service_type'],
274+
'JOB'
275+
>,
276+
}
277+
}
278+
/>
279+
)}
280+
<span className="flex flex-col">
281+
<span className="truncate text-ssm">
282+
<Truncate text={service.identifier.name} truncateLimit={16} />
283+
</span>
284+
</span>
285+
</Link>
286+
</DropdownMenu.Item>
287+
)
288+
})}
289+
</>
290+
))
291+
.otherwise((s) => (
292+
<>
293+
{s.services.map((service, index) => (
241294
<DropdownMenu.Item
242295
key={index}
243296
className="flex h-[50px] w-full items-center gap-2 border-t border-neutral-200 pl-2 pr-3 text-xs text-neutral-400 transition-colors hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-none"
@@ -249,7 +302,11 @@ export function DropdownServices({ environment, deploymentHistory, stages }: Dro
249302
environment.organization.id,
250303
environment.project.id,
251304
environment.id
252-
) + ENVIRONMENT_STAGES_URL()
305+
) +
306+
DEPLOYMENT_LOGS_VERSION_URL(
307+
service.identifier.service_id,
308+
service.identifier.execution_id
309+
)
253310
}
254311
state={{ prevUrl: pathname }}
255312
>
@@ -278,76 +335,42 @@ export function DropdownServices({ environment, deploymentHistory, stages }: Dro
278335
<span className="truncate text-ssm">
279336
<Truncate text={service.identifier.name} truncateLimit={16} />
280337
</span>
338+
{service.total_duration && (
339+
<span
340+
title={dateUTCString(service.auditing_data.updated_at)}
341+
className="text-[11px]"
342+
>
343+
{formatDurationMinutesSeconds(service.total_duration ?? '')}
344+
</span>
345+
)}
281346
</span>
347+
{service.status_details && (
348+
<span className="ml-auto">
349+
<StatusChip status={service.status_details.status} />
350+
</span>
351+
)}
282352
</Link>
283353
</DropdownMenu.Item>
284-
)
285-
})
286-
)
287-
.otherwise((s) =>
288-
s.services.map((service, index) => (
289-
<DropdownMenu.Item
290-
key={index}
291-
className="flex h-[50px] w-full items-center gap-2 border-t border-neutral-200 pl-2 pr-3 text-xs text-neutral-400 transition-colors hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-none"
292-
asChild
293-
>
294-
<Link
295-
to={
296-
ENVIRONMENT_LOGS_URL(
297-
environment.organization.id,
298-
environment.project.id,
299-
environment.id
300-
) +
301-
DEPLOYMENT_LOGS_VERSION_URL(
302-
service.identifier.service_id,
303-
service.identifier.execution_id
304-
)
305-
}
306-
state={{ prevUrl: pathname }}
354+
))}
355+
{(stage.status === 'ERROR' ||
356+
s.services.some((service) => service.status_details?.status === 'ERROR')) && (
357+
<DropdownMenu.Item
358+
className="flex w-full cursor-pointer items-center justify-between gap-2 border-t border-neutral-200 bg-brand-50 px-2 py-2 text-xs text-brand-500 transition-colors hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-none"
359+
onSelect={() => {
360+
const message = 'Why did my deployment fail?'
361+
setDevopsCopilotOpen(true)
362+
sendMessageRef?.current?.(message)
363+
}}
307364
>
308-
{service.details && (
309-
<ServiceAvatar
310-
border="solid"
311-
size="sm"
312-
service={
313-
'job_type' in service.details
314-
? {
315-
icon_uri: service.icon_uri ?? '',
316-
serviceType: 'JOB' as const,
317-
job_type: service.details.job_type as 'CRON' | 'LIFECYCLE',
318-
}
319-
: {
320-
icon_uri: service.icon_uri ?? '',
321-
serviceType: service.identifier.service_type as Exclude<
322-
AnyService['service_type'],
323-
'JOB'
324-
>,
325-
}
326-
}
327-
/>
328-
)}
329-
<span className="flex flex-col">
330-
<span className="truncate text-ssm">
331-
<Truncate text={service.identifier.name} truncateLimit={16} />
332-
</span>
333-
{service.total_duration && (
334-
<span
335-
title={dateUTCString(service.auditing_data.updated_at)}
336-
className="text-[11px]"
337-
>
338-
{formatDurationMinutesSeconds(service.total_duration ?? '')}
339-
</span>
340-
)}
341-
</span>
342-
{service.status_details && (
343-
<span className="ml-auto">
344-
<StatusChip status={service.status_details.status} />
345-
</span>
346-
)}
347-
</Link>
348-
</DropdownMenu.Item>
349-
))
350-
)}
365+
<div className="flex items-center justify-center gap-2">
366+
<Icon iconName="sparkles" iconStyle="solid" className="text-brand-500" />
367+
<span className="font-thin">Ask AI Copilot for diagnostic</span>
368+
</div>
369+
<Icon iconName="arrow-right" />
370+
</DropdownMenu.Item>
371+
)}
372+
</>
373+
))}
351374
</div>
352375
))}
353376
</motion.div>

libs/domains/environments/feature/src/lib/environment-deployment-list/environment-deployment-list.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
} from '@tanstack/react-table'
1212
import clsx from 'clsx'
1313
import { type DeploymentHistoryEnvironmentV2, OrganizationEventOrigin, StateEnum } from 'qovery-typescript-axios'
14-
import { Fragment, useCallback, useMemo, useState } from 'react'
14+
import { Fragment, useCallback, useContext, useMemo, useState } from 'react'
1515
import { useLocation } from 'react-router-dom'
1616
import { P, match } from 'ts-pattern'
17+
import { DevopsCopilotContext } from '@qovery/shared/devops-copilot/context'
1718
import { IconEnum } from '@qovery/shared/enums'
1819
import { ENVIRONMENT_LOGS_URL, ENVIRONMENT_STAGES_URL } from '@qovery/shared/routes'
1920
import {
@@ -74,6 +75,7 @@ export function EnvironmentDeploymentList({ environmentId }: EnvironmentDeployme
7475

7576
const { pathname } = useLocation()
7677
const { openModalConfirmation } = useModalConfirmation()
78+
const { setDevopsCopilotOpen, sendMessageRef } = useContext(DevopsCopilotContext)
7779

7880
const [sorting, setSorting] = useState<SortingState>([])
7981

@@ -278,7 +280,46 @@ export function EnvironmentDeploymentList({ environmentId }: EnvironmentDeployme
278280
/>
279281
<div className="flex flex-col gap-1">
280282
<span className="font-medium text-neutral-400">{upperCaseFirstLetter(trigger_action)}</span>
281-
<span className="text-ssm text-neutral-350">{upperCaseFirstLetter(action_status)}</span>
283+
<div className="flex items-center gap-2">
284+
<span className="text-ssm text-neutral-350">{upperCaseFirstLetter(action_status)}</span>
285+
{action_status === 'ERROR' && (
286+
<Tooltip
287+
classNameContent="rounded-full"
288+
side="bottom"
289+
content={
290+
<div
291+
className="flex cursor-pointer items-center gap-1.5"
292+
onClick={() => {
293+
const message = 'Why did my deployment fail?'
294+
setDevopsCopilotOpen(true)
295+
sendMessageRef?.current?.(message)
296+
}}
297+
>
298+
<Icon iconName="sparkles" iconStyle="solid" className="text-brand-300" />
299+
<span className="text-sm font-thin">Ask AI Copilot for diagnostic</span>
300+
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-white">
301+
<Icon iconName="arrow-right" className="text-neutral-400" />
302+
</div>
303+
</div>
304+
}
305+
>
306+
<div
307+
onClick={() => {
308+
const message = 'Why did my deployment fail?'
309+
setDevopsCopilotOpen(true)
310+
sendMessageRef?.current?.(message)
311+
}}
312+
className="group cursor-pointer"
313+
>
314+
<Icon
315+
iconName="sparkles"
316+
iconStyle="solid"
317+
className="text-neutral-350 transition-colors group-hover:text-brand-500"
318+
/>
319+
</div>
320+
</Tooltip>
321+
)}
322+
</div>
282323
</div>
283324
</div>
284325
)

0 commit comments

Comments
 (0)