Compare commits
4 Commits
@nhost/das
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f0910367d | ||
|
|
e31eefae63 | ||
|
|
abb24afad5 | ||
|
|
18a64555ce |
@@ -1,5 +1,16 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- abb24af: chore: add redirect to support page when project is locked
|
||||
- 18a6455: feat: show contact us info and locked reason when project is locked
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e31eefa: fix: include ingresses field when updating run services
|
||||
|
||||
## 1.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.23.0",
|
||||
"version": "1.24.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
12
dashboard/public/assets/LockedApp.svg
Normal file
12
dashboard/public/assets/LockedApp.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="72" height="73" viewBox="0 0 72 73" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10501_253)">
|
||||
<path d="M0 8.5C0 4.08172 3.58172 0.5 8 0.5H64C68.4183 0.5 72 4.08172 72 8.5V64.5C72 68.9183 68.4183 72.5 64 72.5H8C3.58172 72.5 0 68.9183 0 64.5V8.5Z" fill="#9C73DF" fill-opacity="0.2"/>
|
||||
<path d="M43.1203 35.5H29.7687C28.7153 35.5 27.8613 36.3954 27.8613 37.5V44.5C27.8613 45.6046 28.7153 46.5 29.7687 46.5H43.1203C44.1737 46.5 45.0276 45.6046 45.0276 44.5V37.5C45.0276 36.3954 44.1737 35.5 43.1203 35.5Z" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.6758 35.5V31.5C31.6758 30.1739 32.1782 28.9021 33.0724 27.9645C33.9667 27.0268 35.1795 26.5 36.4442 26.5C37.7089 26.5 38.9217 27.0268 39.816 27.9645C40.7102 28.9021 41.2126 30.1739 41.2126 31.5V35.5" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10501_253">
|
||||
<rect width="72" height="72" fill="white" transform="translate(0 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -8,6 +8,17 @@ import { createTheme as createMuiTheme } from '@mui/material/styles';
|
||||
* @param mode - Color mode
|
||||
* @returns Material UI theme
|
||||
*/
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Palette {
|
||||
beige: Palette['primary'];
|
||||
}
|
||||
|
||||
interface PaletteOptions {
|
||||
beige?: PaletteOptions['primary'];
|
||||
}
|
||||
}
|
||||
|
||||
export default function createTheme(mode: PaletteMode) {
|
||||
return createMuiTheme({
|
||||
shape: {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function PowerOffIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
d="M18.36 6.64A9 9 0 0 1 20.77 15M6.16 6.16a9 9 0 1 0 12.68 12.68M12 2v4M2 2l20 20"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
PowerOffIcon.displayName = 'NhostPowerOffIcon';
|
||||
|
||||
export default PowerOffIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as PowerOffIcon } from './PowerOffIcon';
|
||||
@@ -63,6 +63,9 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
|
||||
paper: '#171d26',
|
||||
},
|
||||
divider: '#2f363d',
|
||||
beige: {
|
||||
main: '#362c22',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,5 +128,8 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
|
||||
paper: '#ffffff',
|
||||
},
|
||||
divider: '#eaedf0',
|
||||
beige: {
|
||||
main: '#e5d1bf',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ApplicationLockedReasonProps {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export default function ApplicationLockedReason({
|
||||
reason,
|
||||
}: ApplicationLockedReasonProps) {
|
||||
return (
|
||||
<Alert severity="warning" className="mx-auto max-w-xs gap-2 p-6 ">
|
||||
<Text className="pb-4 text-left">
|
||||
Your project has been temporarily locked due to the following reason:
|
||||
</Text>
|
||||
<Box
|
||||
className="rounded-md p-2"
|
||||
sx={{
|
||||
backgroundColor: 'beige.main',
|
||||
}}
|
||||
>
|
||||
<Text className="px-2 py-1 font-semibold">{reason}</Text>
|
||||
</Box>
|
||||
<Text className="pt-4 text-left">
|
||||
Please{' '}
|
||||
<Link
|
||||
className="font-semibold underline underline-offset-2"
|
||||
href="/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
contact our support
|
||||
</Link>{' '}
|
||||
team for assistance.
|
||||
</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationLockedReason } from './ApplicationLockedReason';
|
||||
@@ -2,25 +2,24 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { Modal } from '@/components/ui/v1/Modal';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ApplicationInfo } from '@/features/projects/common/components/ApplicationInfo';
|
||||
import { ApplicationLockedReason } from '@/features/projects/common/components/ApplicationLockedReason';
|
||||
import { ApplicationPausedReason } from '@/features/projects/common/components/ApplicationPausedReason';
|
||||
import { ApplicationPausedSymbol } from '@/features/projects/common/components/ApplicationPausedSymbol';
|
||||
import { ChangePlanModal } from '@/features/projects/common/components/ChangePlanModal';
|
||||
import { RemoveApplicationModal } from '@/features/projects/common/components/RemoveApplicationModal';
|
||||
import { StagingMetadata } from '@/features/projects/common/components/StagingMetadata';
|
||||
import { useAppPausedReason } from '@/features/projects/common/hooks/useAppPausedReason';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useGetFreeAndActiveProjectsQuery,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ApplicationPaused() {
|
||||
@@ -28,7 +27,6 @@ export default function ApplicationPaused() {
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const user = useUserData();
|
||||
|
||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
@@ -36,13 +34,8 @@ export default function ApplicationPaused() {
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
variables: { userId: user?.id },
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
|
||||
const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
|
||||
const { isLocked, lockedReason, freeAndLiveProjectsNumberExceeded, loading } =
|
||||
useAppPausedReason();
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
await execPromiseWithErrorToast(
|
||||
@@ -77,75 +70,67 @@ export default function ApplicationPaused() {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-4 text-center">
|
||||
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-6 text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
alt="Closed Eye"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
<ApplicationPausedSymbol isLocked={isLocked} />
|
||||
</div>
|
||||
|
||||
<Box className="grid grid-flow-row gap-1">
|
||||
<Box className="grid grid-flow-row gap-6">
|
||||
<Text variant="h3" component="h1">
|
||||
{currentProject.name} is sleeping
|
||||
{currentProject.name} is {isLocked ? 'locked' : 'paused'}
|
||||
</Text>
|
||||
{isLocked ? (
|
||||
<ApplicationLockedReason reason={lockedReason} />
|
||||
) : (
|
||||
<>
|
||||
<ApplicationPausedReason
|
||||
freeAndLiveProjectsNumberExceeded={
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
{isOwner && (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={
|
||||
changingApplicationStateLoading ||
|
||||
freeAndLiveProjectsNumberExceeded
|
||||
}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
|
||||
<Text>
|
||||
Starter projects stop responding to API calls after 7 days of
|
||||
inactivity. Upgrade to Pro to avoid autosleep.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
{isOwner && (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={changingApplicationStateLoading || wakeUpDisabled}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
|
||||
{wakeUpDisabled && (
|
||||
<Alert severity="warning" className="mx-auto max-w-xs text-left">
|
||||
Note: Only one free project can be active at any given time.
|
||||
Please pause your active free project before unpausing{' '}
|
||||
{currentProject.name}.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<StagingMetadata>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
interface ApplicationPausedReasonProps {
|
||||
freeAndLiveProjectsNumberExceeded?: boolean;
|
||||
}
|
||||
|
||||
export default function ApplicationPausedReason({
|
||||
freeAndLiveProjectsNumberExceeded,
|
||||
}: ApplicationPausedReasonProps) {
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="mx-auto flex max-w-xs flex-col gap-4 p-6 text-left"
|
||||
>
|
||||
<Text>
|
||||
Starter projects will stop responding to API calls after 7 days of
|
||||
inactivity, so consider
|
||||
<span className="font-semibold"> upgrading to Pro </span>to avoid
|
||||
auto-sleep.
|
||||
</Text>
|
||||
{freeAndLiveProjectsNumberExceeded && (
|
||||
<Text>
|
||||
Additionally, only 1 free project can be active at any given time, so
|
||||
please pause your current active free project before unpausing
|
||||
another.
|
||||
</Text>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationPausedReason } from './ApplicationPausedReason';
|
||||
@@ -0,0 +1,23 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function ApplicationPausedSymbol({
|
||||
isLocked,
|
||||
}: {
|
||||
isLocked?: boolean;
|
||||
}) {
|
||||
if (isLocked) {
|
||||
return (
|
||||
<Image src="/assets/LockedApp.svg" alt="Lock" width={72} height={72} />
|
||||
);
|
||||
}
|
||||
|
||||
// paused
|
||||
return (
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
alt="Closed Eye"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ApplicationPausedSymbol } from './ApplicationPausedSymbol';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useAppPausedReason } from './useAppPausedReason';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
|
||||
import {
|
||||
useGetFreeAndActiveProjectsQuery,
|
||||
useGetProjectIsLockedQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
|
||||
/**
|
||||
* This hook returns the reason why the application is paused.
|
||||
* It returns the locked reason and if the user has exceeded the number of free and live projects.
|
||||
*/
|
||||
|
||||
export default function useAppPausedReason(): {
|
||||
isLocked: boolean;
|
||||
lockedReason: string | undefined;
|
||||
freeAndLiveProjectsNumberExceeded: boolean;
|
||||
loading: boolean;
|
||||
} {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const user = useUserData();
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
variables: { userId: user?.id },
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const { data: isLockedData } = useGetProjectIsLockedQuery({
|
||||
variables: { appId: currentProject.id },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
const isLocked = isLockedData?.app?.isLocked;
|
||||
const lockedReason = isLockedData?.app?.isLockedReason;
|
||||
|
||||
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
|
||||
const freeAndLiveProjectsNumberExceeded =
|
||||
numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
|
||||
|
||||
return {
|
||||
isLocked,
|
||||
lockedReason,
|
||||
freeAndLiveProjectsNumberExceeded,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useInsertRunServiceMutation,
|
||||
@@ -99,38 +100,43 @@ export default function ServiceForm({
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const getFormattedConfig = (values: ServiceFormValues) => {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ServiceFormValues;
|
||||
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: values.name,
|
||||
name: sanitizedValues.name,
|
||||
image: {
|
||||
image: values.image,
|
||||
image: sanitizedValues.image,
|
||||
},
|
||||
command: parse(values.command).map((item) => item.toString()),
|
||||
command: parse(sanitizedValues.command).map((item) => item.toString()),
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: values.compute.cpu,
|
||||
memory: values.compute.memory,
|
||||
cpu: sanitizedValues.compute.cpu,
|
||||
memory: sanitizedValues.compute.memory,
|
||||
},
|
||||
storage: values.storage.map((item) => ({
|
||||
storage: sanitizedValues.storage.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
capacity: item.capacity,
|
||||
})),
|
||||
replicas: values.replicas,
|
||||
replicas: sanitizedValues.replicas,
|
||||
},
|
||||
environment: values.environment.map((item) => ({
|
||||
environment: sanitizedValues.environment.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
ports: values.ports.map((item) => ({
|
||||
ports: sanitizedValues.ports.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses,
|
||||
})),
|
||||
healthCheck: values.healthCheck
|
||||
healthCheck: sanitizedValues.healthCheck
|
||||
? {
|
||||
port: values.healthCheck?.port,
|
||||
initialDelaySeconds: values.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: values.healthCheck?.probePeriodSeconds,
|
||||
port: sanitizedValues.healthCheck?.port,
|
||||
initialDelaySeconds:
|
||||
sanitizedValues.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,13 @@ export const validationSchema = Yup.object({
|
||||
port: Yup.number().required(),
|
||||
type: Yup.mixed<PortTypes>().oneOf(Object.values(PortTypes)).required(),
|
||||
publish: Yup.boolean().default(false),
|
||||
ingresses: Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
fqdn: Yup.array().of(Yup.string()),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}),
|
||||
),
|
||||
storage: Yup.array().of(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import { PortTypes } from '@/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import { type ServiceFormValues } from '@/features/services/components/ServiceForm/ServiceFormTypes';
|
||||
import { getRunServicePortURL } from '@/utils/helpers';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
export default function PortsFormSection() {
|
||||
@@ -40,14 +41,8 @@ export default function PortsFormSection() {
|
||||
formValues.ports[index]?.type === PortTypes.HTTP &&
|
||||
formValues.ports[index]?.publish;
|
||||
|
||||
const getPortURL = (_port: string | number, subdomain: string) => {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="space-y-4 rounded border-1 p-4">
|
||||
<Box className="p-4 space-y-4 rounded border-1">
|
||||
<Box className="flex flex-row items-center justify-between ">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
@@ -69,14 +64,14 @@ export default function PortsFormSection() {
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => append({ port: null, type: null, publish: false })}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
<PlusIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -133,16 +128,18 @@ export default function PortsFormSection() {
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{showURL(index) && (
|
||||
<InfoCard
|
||||
title="URL"
|
||||
value={getPortURL(
|
||||
formValues.ports[index]?.port,
|
||||
formValues.subdomain,
|
||||
value={getRunServicePortURL(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region.name,
|
||||
currentProject?.region.domain,
|
||||
formValues.ports[index],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import { getRunServicePortURL } from '@/utils/helpers';
|
||||
import type { ConfigRunServicePort } from '@/utils/__generated__/graphql';
|
||||
|
||||
export interface ServiceDetailsDialogProps {
|
||||
@@ -32,11 +33,7 @@ export default function ServiceDetailsDialog({
|
||||
|
||||
const { closeDialog } = useDialog();
|
||||
|
||||
const getPortURL = (_port: string | number) => {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
|
||||
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
|
||||
};
|
||||
const publishedPorts = ports.filter((port) => port.publish);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-6 pb-6">
|
||||
@@ -48,18 +45,21 @@ export default function ServiceDetailsDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ports?.length > 0 && (
|
||||
{publishedPorts?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text color="secondary">Ports</Text>
|
||||
{ports
|
||||
.filter((port) => port.publish)
|
||||
.map((port) => (
|
||||
<InfoCard
|
||||
key={String(port.port)}
|
||||
title={`${port.type} <--> ${port.port}`}
|
||||
value={getPortURL(port.port)}
|
||||
/>
|
||||
))}
|
||||
{publishedPorts.map((port) => (
|
||||
<InfoCard
|
||||
key={String(port.port)}
|
||||
title={`${port.type} <--> ${port.port}`}
|
||||
value={getRunServicePortURL(
|
||||
subdomain,
|
||||
currentProject?.region.name,
|
||||
currentProject?.region.domain,
|
||||
port,
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function ServicesList({
|
||||
port: item.port,
|
||||
type: item.type as PortTypes,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses,
|
||||
})),
|
||||
compute: service.config?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
|
||||
6
dashboard/src/gql/app/getProjectIsLocked.graphql
Normal file
6
dashboard/src/gql/app/getProjectIsLocked.graphql
Normal file
@@ -0,0 +1,6 @@
|
||||
query getProjectIsLocked($appId: uuid!) {
|
||||
app(id: $appId) {
|
||||
isLocked
|
||||
isLockedReason
|
||||
}
|
||||
}
|
||||
52
dashboard/src/utils/__generated__/graphql.ts
generated
52
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -12522,12 +12522,6 @@ export type Mutation_Root = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootBackupAllApplicationsDatabaseArgs = {
|
||||
expireInDays?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootBackupApplicationDatabaseArgs = {
|
||||
appID: Scalars['String'];
|
||||
@@ -22922,6 +22916,13 @@ export type GetConfiguredVersionsQueryVariables = Exact<{
|
||||
|
||||
export type GetConfiguredVersionsQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', auth?: { __typename?: 'ConfigAuth', version?: string | null } | null, postgres?: { __typename?: 'ConfigPostgres', version?: string | null } | null, hasura: { __typename?: 'ConfigHasura', version?: string | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null, storage?: { __typename?: 'ConfigStorage', version?: string | null } | null } | null };
|
||||
|
||||
export type GetProjectIsLockedQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectIsLockedQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', isLocked?: boolean | null, isLockedReason?: string | null } | null };
|
||||
|
||||
export type GetProjectLocalesQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
@@ -24826,6 +24827,45 @@ export type GetConfiguredVersionsQueryResult = Apollo.QueryResult<GetConfiguredV
|
||||
export function refetchGetConfiguredVersionsQuery(variables: GetConfiguredVersionsQueryVariables) {
|
||||
return { query: GetConfiguredVersionsDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectIsLockedDocument = gql`
|
||||
query getProjectIsLocked($appId: uuid!) {
|
||||
app(id: $appId) {
|
||||
isLocked
|
||||
isLockedReason
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetProjectIsLockedQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetProjectIsLockedQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetProjectIsLockedQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetProjectIsLockedQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetProjectIsLockedQuery(baseOptions: Apollo.QueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
|
||||
}
|
||||
export function useGetProjectIsLockedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
|
||||
}
|
||||
export type GetProjectIsLockedQueryHookResult = ReturnType<typeof useGetProjectIsLockedQuery>;
|
||||
export type GetProjectIsLockedLazyQueryHookResult = ReturnType<typeof useGetProjectIsLockedLazyQuery>;
|
||||
export type GetProjectIsLockedQueryResult = Apollo.QueryResult<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>;
|
||||
export function refetchGetProjectIsLockedQuery(variables: GetProjectIsLockedQueryVariables) {
|
||||
return { query: GetProjectIsLockedDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectLocalesDocument = gql`
|
||||
query getProjectLocales($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
||||
import type {
|
||||
ConfigRunServicePort,
|
||||
DeploymentRowFragment,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import slugify from 'slugify';
|
||||
|
||||
export function getLastLiveDeployment(deployments?: DeploymentRowFragment[]) {
|
||||
@@ -108,3 +111,22 @@ export const removeTypename = (obj: any) => {
|
||||
});
|
||||
return newObj;
|
||||
};
|
||||
|
||||
export const getRunServicePortURL = (
|
||||
subdomain: string,
|
||||
regionName: string,
|
||||
regionDomain: string,
|
||||
port: Partial<ConfigRunServicePort>,
|
||||
) => {
|
||||
const { port: servicePort, ingresses } = port;
|
||||
|
||||
const customDomain = ingresses?.[0]?.fqdn?.[0];
|
||||
|
||||
if (customDomain) {
|
||||
return `https://${customDomain}`;
|
||||
}
|
||||
|
||||
const servicePortNumber =
|
||||
Number(servicePort) > 0 ? Number(servicePort) : '[port]';
|
||||
return `https://${subdomain}-${servicePortNumber}.svc.${regionName}.${regionDomain}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user