Skip to content
Merged
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 @@ -86,8 +86,6 @@ export function UserMenu() {

<DropdownMenu.Separator />

<DropdownMenu.Item>Give feedback</DropdownMenu.Item>

<DropdownMenu.Item asChild>
<a
href="https://www.qovery.com"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { type IconName } from '@fortawesome/fontawesome-common-types'
import { Outlet, createFileRoute, useMatchRoute } from '@tanstack/react-router'
import { Link as RouterLink } from '@tanstack/react-router'
import { ClusterAvatar, useCluster } from '@qovery/domains/clusters/feature'
import {
EnvironmentLastDeploymentSection,
EnvironmentMode,
EnvironmentStateChip,
MenuManageDeployment,
MenuOtherActions,
useDeploymentStatus,
useEnvironment,
} from '@qovery/domains/environments/feature'
import { Heading, Icon, Link, Navbar, Section } from '@qovery/shared/ui'
import { Heading, Icon, Link, Navbar, Section, Tooltip } from '@qovery/shared/ui'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/overview'
Expand All @@ -22,6 +25,7 @@ function RouteComponent() {

const { data: environment } = useEnvironment({ environmentId, suspense: true })
const { data: deploymentStatus } = useDeploymentStatus({ environmentId })
const { data: cluster } = useCluster({ organizationId, clusterId: environment?.cluster_id, suspense: true })

const tabs = [
{
Expand All @@ -47,13 +51,26 @@ function RouteComponent() {
<div className="container mx-auto mt-6 pb-10">
<Section className="gap-8">
<div className="flex flex-col gap-6">
<div className="flex justify-between">
<div className="flex items-center gap-3">
<EnvironmentMode mode={environment.mode} variant="shrink" />
<Heading>{environment?.name}</Heading>
<div className="flex justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<EnvironmentMode mode={environment.mode} variant="shrink" className="mr-1" />
<Tooltip content={environment.name}>
<Heading className="min-w-0 max-w-full truncate">{environment.name}</Heading>
</Tooltip>
<EnvironmentStateChip className="ml-0.5 shrink-0" mode="running" environmentId={environment.id} />
<span className="ml-2 mr-0.5 h-4 w-px shrink-0 bg-surface-neutral-component" />
<RouterLink
to="/organization/$organizationId/cluster/$clusterId/overview"
params={{ organizationId, clusterId: environment.cluster_id }}
className="group flex shrink-0 items-center gap-1 text-ssm"
color="neutral"
>
<ClusterAvatar cluster={cluster} size="sm" />
<span className="group-hover:underline">{environment.cluster_name}</span>
</RouterLink>
</div>

<div className="flex gap-2">
<div className="flex shrink-0 gap-2">
<MenuOtherActions
environment={environment}
state={deploymentStatus.last_deployment_state}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createFileRoute, useParams } from '@tanstack/react-router'
import posthog from 'posthog-js'
import { useCallback, useEffect, useMemo } from 'react'
import { match } from 'ts-pattern'
import { useCluster, useClusterRunningStatus, useClusterStatus } from '@qovery/domains/clusters/feature'
import { useCluster, useClusterStatus } from '@qovery/domains/clusters/feature'
import { useEnvironment } from '@qovery/domains/environments/feature'
import {
EnableObservabilityButtonContactUs,
Expand Down Expand Up @@ -33,10 +33,6 @@ function RouteComponent() {
const { data: serviceStatus } = useDeploymentStatus({ environmentId, serviceId })
const { data: service } = useService({ environmentId, serviceId, suspense: true })

const { data: clusterRunningStatus } = useClusterRunningStatus({
organizationId: environment?.organization.id ?? '',
clusterId: environment?.cluster_id ?? '',
})
const { data: clusterStatus } = useClusterStatus({
organizationId: environment?.organization.id ?? '',
clusterId: environment?.cluster_id ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ jest.mock('@tanstack/react-router', () => ({
Link: ({ children, ...props }: { children?: ReactNode; [key: string]: unknown }) => <a {...props}>{children}</a>,
}))

jest.mock('react-responsive', () => ({
useMediaQuery: () => false,
}))

jest.mock('../../hooks/use-environments/use-environments', () => ({
__esModule: true,
default: () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Link, useNavigate, useParams } from '@tanstack/react-router'
import { EnvironmentModeEnum, type EnvironmentOverviewResponse } from 'qovery-typescript-axios'
import { type KeyboardEvent, type MouseEvent } from 'react'
import { useMediaQuery } from 'react-responsive'
import { match } from 'ts-pattern'
import { ClusterAvatar } from '@qovery/domains/clusters/feature'
import { Button, DeploymentAction, Heading, Icon, Section, TablePrimitives, Truncate } from '@qovery/shared/ui'
import { Button, DeploymentAction, Heading, Icon, Section, TablePrimitives, Tooltip } from '@qovery/shared/ui'
import { timeAgo } from '@qovery/shared/util-dates'
import { pluralize, twMerge } from '@qovery/shared/util-js'
import { MenuManageDeployment, MenuOtherActions } from '../../environment-action-toolbar/environment-action-toolbar'
Expand All @@ -23,13 +22,6 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
const { data: environments = [] } = useEnvironments({ projectId, suspense: true })
const environment = environments.find((env) => env.id === overview.id)
const cellClassName = 'h-auto border-l border-neutral py-2'
const isDesktopOrLaptop = useMediaQuery({
query: '(min-width: 1280px)',
})
const isVeryLargeScreen = useMediaQuery({
query: '(min-width: 1536px)',
})

const stopRowNavigation = (event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => {
event.stopPropagation()
}
Expand All @@ -56,10 +48,10 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
}}
>
<Table.Cell className={twMerge(cellClassName, 'border-none p-0')}>
<div className="flex h-full flex-col justify-center gap-1 px-4 py-2 xl:flex-row xl:items-center xl:justify-between xl:gap-2">
<span className="text-wrap break-all text-sm font-medium">
<Truncate text={overview.name} truncateLimit={isVeryLargeScreen ? 72 : isDesktopOrLaptop ? 40 : 30} />
</span>
<div className="flex h-full min-w-0 flex-col justify-center gap-1 px-4 py-2 xl:flex-row xl:items-center xl:justify-between xl:gap-2">
<Tooltip content={overview.name}>
<span className="block min-w-0 flex-1 truncate text-sm font-medium">{overview.name}</span>
</Tooltip>
<div className="flex flex-shrink-0 items-center gap-2">
<span className="font-normal text-neutral-subtle">
{overview.service_count} {pluralize(overview.service_count, 'service')}
Expand All @@ -71,10 +63,16 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
<Table.Cell className={cellClassName}>
<div className="flex h-full items-center justify-between">
<div className="flex flex-col gap-1 xl:flex-row xl:items-center xl:gap-2">
<DeploymentAction status={overview.deployment_status?.last_deployment_state} />
<span className="text-neutral-subtle">
{timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago
</span>
{overview.service_count > 0 ? (
<>
<DeploymentAction status={overview.deployment_status?.last_deployment_state} />
<span className="text-neutral-subtle">
{timeAgo(new Date(overview.deployment_status?.last_deployment_date ?? Date.now()))} ago
</span>
</>
) : (
<span className="text-sm text-neutral-subtle">No services yet</span>
)}
</div>
<EnvironmentStateChip mode="last-deployment" environmentId={overview.id} variant="monochrome" />
</div>
Expand All @@ -87,12 +85,12 @@ function EnvRow({ overview }: { overview: EnvironmentOverviewResponse }) {
params={{ organizationId, clusterId: overview.cluster.id }}
onClick={stopRowNavigation}
onKeyDown={stopRowNavigation}
className="group lg:inline-flex lg:items-center lg:gap-2"
className="group min-w-0 lg:inline-flex lg:items-center lg:gap-2"
>
<ClusterAvatar cluster={overview.cluster} size="sm" className="hidden lg:inline-block" />
<span className="text-wrap break-all group-hover:underline">
<Truncate text={overview.cluster?.name} truncateLimit={isDesktopOrLaptop ? 40 : 20} />
</span>
<Tooltip content={overview.cluster.name}>
<span className="block min-w-0 truncate group-hover:underline">{overview.cluster.name}</span>
</Tooltip>
</Link>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ exports[`ServiceList should match snapshot 1`] = `
<th
class="px-4 text-left font-code font-normal relative flex h-full items-center border-r border-neutral text-neutral-subtle last:border-r-0"
>
Last deployment
Last operation
</th>
<th
class="px-4 text-left font-code font-normal relative flex h-full items-center border-r border-neutral text-neutral-subtle last:border-r-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function ServiceLastDeploymentCell({ service, environment }: ServiceLastD
)

return deploymentStatus?.state === 'READY' ? (
<span className="text-sm font-normal text-neutral-subtle">Never been deployed</span>
<span className="text-sm font-normal text-neutral-subtle">No operations yet</span>
) : (
<div className="flex w-full items-center justify-between gap-4">
<WrappingLink>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function ServiceList({ className, containerClassName, environment, ...pro
}),
columnHelper.display({
id: 'last_deployment',
header: 'Last deployment',
header: 'Last operation',
enableColumnFilter: false,
enableSorting: false,
cell: (info) => <ServiceLastDeploymentCell service={info.row.original} environment={environment} />,
Expand Down Expand Up @@ -290,12 +290,16 @@ export function ServiceList({ className, containerClassName, environment, ...pro

const ServicesBadges = useCallback(() => {
const getLabel = (value: string, count: number) => {
const statusLabel = value.toLowerCase()
const statusLabel = value.toLowerCase().split('_').join(' ')
const isErrorStatus = value === 'ERROR' || value.endsWith('_ERROR')

return match(value)
.with('RUNNING', 'STOPPED', () => `${count} ${statusLabel}`)
.with('ERROR', () => `${count} in error`)
.otherwise(() => `${count} ${pluralize(count, statusLabel)}`)
.with('WARNING', () => `${count} ${pluralize(count, 'warning')}`)
.when(
() => isErrorStatus,
() => `${count} in error`
)
.otherwise(() => `${count} ${statusLabel}`)
}

return statusFacetedUniqueValues.some(([value]) => value === undefined) ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link, useParams } from '@tanstack/react-router'
import { type ApplicationGitRepository, type Credentials, type Environment } from 'qovery-typescript-axios'
import { P, match } from 'ts-pattern'
import { ClusterAvatar, useCluster } from '@qovery/domains/clusters/feature'
import { type AnyService } from '@qovery/domains/services/data-access'
import {
IconEnum,
Expand All @@ -10,7 +11,7 @@ import {
isJobContainerSource,
isJobGitSource,
} from '@qovery/shared/enums'
import { Badge, Button, ExternalLink, Heading, Icon, ToastEnum, Truncate, toast } from '@qovery/shared/ui'
import { Badge, Button, ExternalLink, Heading, Icon, ToastEnum, Tooltip, Truncate, toast } from '@qovery/shared/ui'
import { buildGitProviderUrl } from '@qovery/shared/util-git'
import { useCopyToClipboard } from '@qovery/shared/util-hooks'
import { containerRegistryKindToIcon, upperCaseFirstLetter } from '@qovery/shared/util-js'
Expand Down Expand Up @@ -67,6 +68,7 @@ export interface ServiceHeaderProps {
function ServiceHeaderContent({ environment, serviceId, service }: ServiceHeaderProps) {
const { organizationId = '', projectId = '' } = useParams({ strict: false })
const { data: masterCredentials } = useMasterCredentials({ serviceId, serviceType: service?.serviceType })
const { data: cluster } = useCluster({ organizationId, clusterId: environment.cluster_id, suspense: true })

const [, copyToClipboard] = useCopyToClipboard()

Expand Down Expand Up @@ -105,8 +107,8 @@ function ServiceHeaderContent({ environment, serviceId, service }: ServiceHeader
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2">
<ServiceAvatar
size="sm"
radius="none"
Expand All @@ -124,15 +126,22 @@ function ServiceHeaderContent({ environment, serviceId, service }: ServiceHeader
}
}
/>
<Heading>{service.name}</Heading>
<ServiceStateChip className="ml-0.5" mode="running" environmentId={environment.id} serviceId={serviceId} />
<span className="mx-2 h-4 w-px bg-surface-neutral-component" />
<Tooltip content={service.name}>
<Heading className="min-w-0 max-w-full truncate">{service.name}</Heading>
</Tooltip>
<ServiceStateChip
className="ml-0.5 shrink-0"
mode="running"
environmentId={environment.id}
serviceId={serviceId}
/>
<span className="ml-2 mr-0.5 h-4 w-px shrink-0 bg-surface-neutral-component" />
<Link
to="/organization/$organizationId/cluster/$clusterId/overview"
params={{ organizationId, clusterId: environment.cluster_id }}
className="group flex items-center gap-2 text-ssm"
className="group flex shrink-0 items-center gap-1 text-ssm"
>
<Icon className="w-5" name={environment.cloud_provider.provider} />
<ClusterAvatar cluster={cluster} size="sm" />
<span className="group-hover:underline">{environment.cluster_name}</span>
</Link>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,11 @@ exports[`VariableList should match snapshot 1`] = `
class="inline-flex items-center gap-10 rounded-md border border-neutral bg-surface-neutralInvert-component p-2 pl-4 text-neutralInvert shadow-xl animate-action-bar-fade-out hidden"
data-testid="sticky-action-form-toaster"
>
<span
class="text-sm font-medium text-neutralInvert"
>
0 selected variable
</span>
<div
class="flex gap-5"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function VariableListActionBar({ selectedRows = [], resetRowSelection }:
const deletableCount = deletableVariables.length
const description =
selectedCount === 0
? ''
? '0 selected variable'
: `${selectedCount} selected ${pluralize(selectedCount, 'variable')}${
deletableCount === selectedCount ? '' : ` (${deletableCount} deletable)`
}`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Dialog from '@radix-ui/react-dialog'
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import { UserSettingsModal } from './user-settings-modal'

Expand Down Expand Up @@ -33,12 +34,21 @@ jest.mock('../use-user-edit-account/use-user-edit-account', () => ({
}))

describe('UserSettingsModal', () => {
const renderUserSettingsModal = () =>
renderWithProviders(
<Dialog.Root open>
<Dialog.Content>
<UserSettingsModal />
</Dialog.Content>
</Dialog.Root>
)

beforeEach(() => {
mockMutateAsync.mockResolvedValue(undefined)
})

it('should render user info and form fields', async () => {
renderWithProviders(<UserSettingsModal />)
renderUserSettingsModal()
await waitFor(() => expect(screen.getByRole('button', { name: /save/i })).toBeEnabled())

expect(screen.getByText('John Doe')).toBeInTheDocument()
Expand All @@ -51,7 +61,7 @@ describe('UserSettingsModal', () => {
})

it('should call mutateAsync with updated email on submit', async () => {
const { userEvent } = renderWithProviders(<UserSettingsModal />)
const { userEvent } = renderUserSettingsModal()
await waitFor(() => expect(screen.getByRole('button', { name: /save/i })).toBeEnabled())
const emailInput = screen.getByLabelText(/communication email/i)
const saveButton = screen.getByRole('button', { name: /save/i })
Expand All @@ -67,7 +77,7 @@ describe('UserSettingsModal', () => {
})

it('should show validation error for invalid email', async () => {
const { userEvent } = renderWithProviders(<UserSettingsModal />)
const { userEvent } = renderUserSettingsModal()
await waitFor(() => expect(screen.getByRole('button', { name: /save/i })).toBeEnabled())
const emailInput = screen.getByLabelText(/communication email/i)

Expand All @@ -78,7 +88,7 @@ describe('UserSettingsModal', () => {
})

it('should disable Save button when form is invalid', async () => {
const { userEvent } = renderWithProviders(<UserSettingsModal />)
const { userEvent } = renderUserSettingsModal()
await waitFor(() => expect(screen.getByRole('button', { name: /save/i })).toBeEnabled())
const emailInput = screen.getByLabelText(/communication email/i)
const saveButton = screen.getByRole('button', { name: /save/i })
Expand Down
Loading
Loading