Skip to content
Open
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
7 changes: 7 additions & 0 deletions apps/console/public/assets/ai-tools/claude.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/console/public/assets/ai-tools/codex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions apps/console/public/assets/ai-tools/cursor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/console/public/assets/ai-tools/gemini.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/console/public/assets/ai-tools/opencode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion apps/console/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type QueryClient } from '@tanstack/react-query'
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
import { ModalProvider, ToastBehavior } from '@qovery/shared/ui'
import { McpSuggestionPortal, ModalProvider, ToastBehavior } from '@qovery/shared/ui'
import { type Auth0ContextType } from '../auth/auth0'

interface RouterContext {
Expand All @@ -13,6 +13,7 @@ const RootLayout = () => {
<ModalProvider>
<Outlet />
<ToastBehavior />
<McpSuggestionPortal />
</ModalProvider>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
import { type ClusterAdvancedSettings } from 'qovery-typescript-axios'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import { ClusterAdvancedSettings as ClusterAdvancedSettingsComponent } from './cluster-advanced-settings'

const mockClusterAdvancedSettings = {
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('ClusterAdvancedSettings', () => {

expect(screen.getByTestId('sticky-action-form-toaster')).toBeInTheDocument()
const toaster = screen.getByTestId('sticky-action-form-toaster')
expect(toaster).toHaveClass('visible')
await waitFor(() => expect(toaster).toHaveClass('visible'))
})

it('should not show StickyActionFormToaster when form is not dirty', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import clsx from 'clsx'
import { type Cluster, type ClusterStatus, type Project } from 'qovery-typescript-axios'
import { match } from 'ts-pattern'
import { useProjects } from '@qovery/domains/projects/feature'
import { AnimatedGradientText, Icon, Link, LogoIcon } from '@qovery/shared/ui'
import { AnimatedGradientText, FloatingStackPortal, Icon, Link, LogoIcon } from '@qovery/shared/ui'
import { twMerge } from '@qovery/shared/util-js'
import { useDeploymentProgress } from './use-deployment-progress'

Expand All @@ -23,15 +23,17 @@ export function ClusterDeploymentProgressCard({
if (!clusters.length) return null

return (
<div className="fixed bottom-5 right-4 w-96 max-w-full overflow-hidden rounded-xl border border-neutral bg-surface-neutral shadow-md">
<AccordionPrimitive.Root type="multiple" className="w-full">
{clusters.map((cluster) => {
const clusterStatus = clusterStatuses.find(({ cluster_id }) => cluster_id === cluster.id)
<FloatingStackPortal position="top">
<div className="w-96 max-w-full overflow-hidden rounded-xl border border-neutral bg-surface-neutral shadow-md">
<AccordionPrimitive.Root type="multiple" className="w-full">
{clusters.map((cluster) => {
const clusterStatus = clusterStatuses.find(({ cluster_id }) => cluster_id === cluster.id)

return <Item key={cluster.id} cluster={cluster} clusterStatus={clusterStatus} project={projects[0]} />
})}
</AccordionPrimitive.Root>
</div>
return <Item key={cluster.id} cluster={cluster} clusterStatus={clusterStatus} project={projects[0]} />
})}
</AccordionPrimitive.Root>
</div>
</FloatingStackPortal>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/clusters/data-access'
import { showMcpSuggestionToast } from '@qovery/shared/ui'
import { queries } from '@qovery/state/util-queries'

export function useCreateCluster() {
const queryClient = useQueryClient()

return useMutation(mutations.createCluster, {
onSuccess(_, { organizationId }) {
onSuccess(data, { organizationId }) {
queryClient.invalidateQueries({
queryKey: queries.clusters.list({ organizationId }).queryKey,
})
showMcpSuggestionToast({ type: 'cluster', name: data.name, clusterType: data.cloud_provider })
},
meta: {
notifyOnSuccess: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ describe('SectionProductionHealth', () => {
renderWithProviders(<SectionProductionHealth />)

expect(screen.getByRole('heading', { name: 'Production health' })).toBeInTheDocument()
expect(screen.getByText('Let your agent do the configuration with')).toBeInTheDocument()
expect(screen.getByText('Just install our AI skills and ask your agent to get you started!')).toBeInTheDocument()
expect(screen.getByText('curl -fsSL https://skill.qovery.com/install.sh | bash')).toBeInTheDocument()
expect(screen.getByText('Qovery managed')).toBeInTheDocument()
expect(screen.getByText('Bring your own cluster')).toBeInTheDocument()
expect(screen.getByText('Local machine (demo)')).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link as RouterLink, useParams } from '@tanstack/react-router'
import clsx from 'clsx'
import { type ReactNode, useMemo } from 'react'
import { IconEnum } from '@qovery/shared/enums'
import { EmptyState, Heading, Icon, Link, LogoIcon, Section, useModal } from '@qovery/shared/ui'
import { EmptyState, Heading, Icon, Link, LogoIcon, McpSuggestionCard, Section, useModal } from '@qovery/shared/ui'
import { twMerge } from '@qovery/shared/util-js'
import { AddCreditCardModalFeature } from '../add-credit-card-modal-feature/add-credit-card-modal-feature'
import { ClusterInstallationGuideModal } from '../cluster-installation-guide-modal/cluster-installation-guide-modal'
Expand Down Expand Up @@ -211,64 +211,71 @@ export function SectionProductionHealth() {
</Link>
</EmptyState>
) : (
<div className="flex flex-col gap-5 rounded-lg border border-neutral bg-surface-neutral p-4 text-sm">
<div className="flex flex-col gap-1">
<p className="font-medium text-neutral">
Install your first cluster and start tracking your production health
</p>
<p className="text-neutral-subtle">
Create a cluster on your cloud provider to be able to deploy apps later
</p>
</div>
<div className="grid gap-3 lg:grid-cols-3">
{CLUSTERS_OPTIONS.map((option) =>
option.action === 'create-cluster' ? (
<RouterLink
key={option.title}
to="/organization/$organizationId/cluster/new"
params={{ organizationId }}
data-action={option.dataAction}
className={getOptionCardClassName(option.highlight)}
>
{renderOptionCardContent(option)}
</RouterLink>
) : (
<button
key={option.title}
type="button"
data-action={option.dataAction}
onClick={() =>
option.requiresCreditCardOnFreeTrial && isNoCreditCardRestriction
? openCreditCardModal()
: openInstallationGuideModal({ isDemo: option.isDemo })
}
className={getOptionCardClassName(option.highlight)}
>
{renderOptionCardContent(option)}
</button>
)
)}
</div>
<div className="flex flex-col gap-2">
<p className="font-medium text-neutral">Related docs</p>
<div className="overflow-hidden rounded border border-neutral">
{RELATED_DOCUMENTATION.map((doc) => (
<a
key={doc.title}
href={doc.url}
title={doc.title}
target="_blank"
rel="noreferrer"
data-action="org-overview__cluster-doc"
className="group flex h-12 w-full items-center justify-between border-b border-neutral p-4 text-ssm text-neutral transition-colors last:border-b-0 hover:bg-surface-neutral-subtle focus:outline-none focus-visible:bg-surface-neutral-subtle"
>
{doc.title}
<Icon
iconName="external-link"
className="text-xs text-neutral-subtle transition-colors group-hover:text-neutral"
/>
</a>
))}
<div className="flex flex-col gap-3">
<McpSuggestionCard
variant="setup"
title="Let your agent do the configuration with"
description="Just install our AI skills and ask your agent to get you started!"
/>
<div className="flex flex-col gap-5 rounded-lg border border-neutral bg-surface-neutral p-4 text-sm">
<div className="flex flex-col gap-1">
<p className="font-medium text-neutral">
Install your first cluster and start tracking your production health
</p>
<p className="text-neutral-subtle">
Create a cluster on your cloud provider to be able to deploy apps later
</p>
</div>
<div className="grid gap-3 lg:grid-cols-3">
{CLUSTERS_OPTIONS.map((option) =>
option.action === 'create-cluster' ? (
<RouterLink
key={option.title}
to="/organization/$organizationId/cluster/new"
params={{ organizationId }}
data-action={option.dataAction}
className={getOptionCardClassName(option.highlight)}
>
{renderOptionCardContent(option)}
</RouterLink>
) : (
<button
key={option.title}
type="button"
data-action={option.dataAction}
onClick={() =>
option.requiresCreditCardOnFreeTrial && isNoCreditCardRestriction
? openCreditCardModal()
: openInstallationGuideModal({ isDemo: option.isDemo })
}
className={getOptionCardClassName(option.highlight)}
>
{renderOptionCardContent(option)}
</button>
)
)}
</div>
<div className="flex flex-col gap-2">
<p className="font-medium text-neutral">Related docs</p>
<div className="overflow-hidden rounded border border-neutral">
{RELATED_DOCUMENTATION.map((doc) => (
<a
key={doc.title}
href={doc.url}
title={doc.title}
target="_blank"
rel="noreferrer"
data-action="org-overview__cluster-doc"
className="group flex h-12 w-full items-center justify-between border-b border-neutral p-4 text-ssm text-neutral transition-colors last:border-b-0 hover:bg-surface-neutral-subtle focus:outline-none focus-visible:bg-surface-neutral-subtle"
>
{doc.title}
<Icon
iconName="external-link"
className="text-xs text-neutral-subtle transition-colors group-hover:text-neutral"
/>
</a>
))}
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/environments/data-access'
import { showMcpSuggestionToast } from '@qovery/shared/ui'
import { queries } from '@qovery/state/util-queries'

export function useCreateEnvironment() {
const queryClient = useQueryClient()

return useMutation(mutations.createEnvironment, {
onSuccess(_, { projectId }) {
onSuccess(data, { projectId }) {
queryClient.invalidateQueries({
queryKey: queries.environments.list({ projectId }).queryKey,
})
queryClient.invalidateQueries({
queryKey: queries.projects.environmentsOverview({ projectId }).queryKey,
})
showMcpSuggestionToast({ type: 'environment', name: data.name, environmentType: data.mode })
},
meta: {
notifyOnSuccess: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ describe('ShowUsageModal', () => {
expect(baseElement).toBeTruthy()
})

it('should render the report form and info callout', () => {
renderWithProviders(wrapWithReactHookForm(<ShowUsageModal {...modalProps} />))

expect(screen.queryByText('Try optimizing your costs with')).not.toBeInTheDocument()
expect(screen.queryByText('Or create a report')).not.toBeInTheDocument()
expect(screen.getByLabelText('Report period')).toBeInTheDocument()
expect(screen.getByText('The report generation can take a few seconds.')).toBeInTheDocument()
})

it('should call onSubmit', async () => {
const onSubmit = jest.fn((event) => event.preventDefault())
const reportPeriods = getReportPeriods({ organization: mockOrganization, orgRenewalAt: modalProps.renewalAt })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { type OrganizationCurrentCost } from 'qovery-typescript-axios'
import { type Organization } from 'qovery-typescript-axios'
import { FormProvider, useForm } from 'react-hook-form'
import { Controller, useFormContext } from 'react-hook-form'
import { useModal } from '@qovery/shared/ui'
import { Callout, Icon, InputSelect, InputText, ModalCrud } from '@qovery/shared/ui'
import { Callout, Icon, InputSelect, InputText, ModalCrud, useModal } from '@qovery/shared/ui'
import { setDayOfTheMonth } from '@qovery/shared/util-dates'
import { useGenerateBillingUsageReport } from '../../hooks/use-generate-billing-usage-report/use-generate-billing-usage-report'
import { useOrganization } from '../../hooks/use-organization/use-organization'
Expand Down Expand Up @@ -113,23 +112,15 @@ export function ShowUsageModal({ organizationId, renewalAt, onSubmit, onClose, l
onClose={onClose}
loading={loading}
>
<Callout.Root className="mb-5" color="yellow">
<Callout.Icon>
<Icon iconName="triangle-exclamation" iconStyle="regular" />
</Callout.Icon>
<Callout.Text className="flex items-center">
The report generation could take a few seconds, please be patient.
</Callout.Text>
</Callout.Root>
<Controller
name="report_period"
control={control}
defaultValue={reportPeriods[0]?.value}
render={({ field, fieldState: { error } }) => (
<InputSelect
dataTestId="input-select-report-period"
className="mb-2"
label="Report period"
className="mb-5"
options={reportPeriods}
onChange={field.onChange}
value={field.value}
Expand All @@ -152,7 +143,7 @@ export function ShowUsageModal({ organizationId, renewalAt, onSubmit, onClose, l
render={({ field, fieldState: { error } }) => (
<InputText
dataTestId="input-expires"
className="mb-5"
className="mb-2"
name={field.name}
type="number"
onChange={field.onChange}
Expand All @@ -162,6 +153,12 @@ export function ShowUsageModal({ organizationId, renewalAt, onSubmit, onClose, l
/>
)}
/>
<Callout.Root className="p-3" color="sky">
<Callout.Icon>
<Icon iconName="circle-info" iconStyle="regular" />
</Callout.Icon>
<Callout.Text>The report generation can take a few seconds.</Callout.Text>
</Callout.Root>
</ModalCrud>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/projects/data-access'
import { showMcpSuggestionToast } from '@qovery/shared/ui'
import { queries } from '@qovery/state/util-queries'

export function useCreateProject({ silently = false } = {}) {
const queryClient = useQueryClient()

return useMutation(mutations.createProject, {
onSuccess(_, { organizationId }) {
onSuccess(data, { organizationId }) {
queryClient.invalidateQueries({
queryKey: queries.projects.list({ organizationId }).queryKey,
})
if (!silently) {
showMcpSuggestionToast({ type: 'project', name: data.name })
}
},
meta: {
...(silently
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { mutations } from '@qovery/domains/services/data-access'
import { showMcpSuggestionToast } from '@qovery/shared/ui'
import { queries } from '@qovery/state/util-queries'

export function useCreateService({ organizationId }: { organizationId: string }) {
const queryClient = useQueryClient()

return useMutation(mutations.createService, {
onSuccess(response) {
onSuccess(response, { payload }) {
queryClient.invalidateQueries({
queryKey: queries.services.list(response.environment.id).queryKey,
})
showMcpSuggestionToast({ type: 'service', name: response.name, serviceType: payload.serviceType })

// gitTokens requests
queryClient.invalidateQueries({
Expand Down
Loading
Loading