Compare commits

..

4 Commits

Author SHA1 Message Date
github-actions[bot]
2f0910367d chore: update versions (#2794)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @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

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-15 15:51:18 +01:00
Hassan Ben Jobrane
e31eefae63 fix(dashboard): include ingresses field when updating a run service (#2798)
### **User description**
fixes https://github.com/nhost/nhost/issues/2797


___

### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Added `ingresses` field to various components and validation schema to
support custom domains.
- Introduced `removeTypename` utility function to sanitize GraphQL
response objects.
- Replaced `getPortURL` with `getRunServicePortURL` helper function for
consistent URL generation.
- Updated changeset to document the inclusion of the `ingresses` field.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServiceForm.tsx</strong><dd><code>Add ingresses field
and sanitize values in ServiceForm</code>&nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/features/services/components/ServiceForm/ServiceForm.tsx

<li>Added <code>removeTypename</code> utility function to sanitize
values.<br> <li> Included <code>ingresses</code> field in the ports
mapping.<br> <li> Updated health check and other fields to use sanitized
values.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-d62640c5c152c7b50a3a53deefcb29c6ed1fa685e15511863c09784497139c49">+19/-13</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServiceFormTypes.ts</strong><dd><code>Update validation
schema to include ingresses field</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/ServiceFormTypes.ts

<li>Added <code>ingresses</code> field to the validation schema.<br>
<li> Made <code>ingresses</code> field nullable.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-70dc64b40f78adad0ce3db0f56cddfe824f3eb2d116b2ea6411518546810f3af">+7/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>PortsFormSection.tsx</strong><dd><code>Use helper
function for port URL generation in
PortsFormSection</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSection.tsx

<li>Replaced <code>getPortURL</code> with
<code>getRunServicePortURL</code> helper function.<br> <li> Minor
formatting changes.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-64ce17ad73e4122e8c66a1968b6737ec98bd1623ac7e3cd3f4a34b549a78717b">+10/-13</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServiceDetailsDialog.tsx</strong><dd><code>Use helper
function for port URL generation in
ServiceDetailsDialog</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/ServiceDetailsDialog/ServiceDetailsDialog.tsx

<li>Replaced <code>getPortURL</code> with
<code>getRunServicePortURL</code> helper function.<br> <li> Filtered and
displayed only published ports.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-2e157263deeb076634b004143232a0f97d3ab94e709c0dcf7e93fb09a62f267d">+15/-15</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServicesList.tsx</strong><dd><code>Include ingresses
field in ServicesList ports mapping</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/features/services/components/ServicesList/ServicesList.tsx

- Included `ingresses` field in the ports mapping.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-efb3008c23436b2db5bb94de15e91c78cf76ef6481ecb02eb542cf660ba98653">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>helpers.ts</strong><dd><code>Add helper functions for
port URL generation and typename removal</code></dd></summary>
<hr>

dashboard/src/utils/helpers/helpers.ts

<li>Added <code>getRunServicePortURL</code> helper function.<br> <li>
Enhanced <code>removeTypename</code> function.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-f640e7215f5f5ea78bbf43fa96267ecdd677214f0dd1d5e0d37bae8c4181a328">+23/-1</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>short-radios-retire.md</strong><dd><code>Add changeset
for ingresses field inclusion</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/short-radios-retire.md

- Added changeset for including `ingresses` field in run services.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-f738014a2859f7ce7160422ab65bfaffd0d81f8e603a46febb468ac05f6087c0">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    
</table></td></tr></tr></tbody></table>

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-15 15:38:04 +01:00
Zephyr (David B.M.)
abb24afad5 chore (dashboard): locked project contact support redirect (#2795) 2024-07-09 20:25:16 +02:00
Zephyr (David B.M.)
18a64555ce feat (dashboard): show contact us info when project is locked (#2775)
Resolves #2624
2024-07-09 15:11:58 +02:00
24 changed files with 399 additions and 124 deletions

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.23.0",
"version": "1.24.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View 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

View File

@@ -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: {

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default as PowerOffIcon } from './PowerOffIcon';

View File

@@ -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',
},
};
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { default as ApplicationLockedReason } from './ApplicationLockedReason';

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { default as ApplicationPausedReason } from './ApplicationPausedReason';

View File

@@ -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}
/>
);
}

View File

@@ -0,0 +1 @@
export { default as ApplicationPausedSymbol } from './ApplicationPausedSymbol';

View File

@@ -0,0 +1 @@
export { default as useAppPausedReason } from './useAppPausedReason';

View File

@@ -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,
};
}

View File

@@ -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,
};

View File

@@ -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(

View File

@@ -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],
)}
/>
)}

View File

@@ -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>
)}

View File

@@ -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,

View File

@@ -0,0 +1,6 @@
query getProjectIsLocked($appId: uuid!) {
app(id: $appId) {
isLocked
isLockedReason
}
}

View File

@@ -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) {

View File

@@ -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}`;
};