Compare commits
26 Commits
@nhost/das
...
@nhost/apo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fd1a54a5 | ||
|
|
a19b85c8ac | ||
|
|
4e1aaca0ee | ||
|
|
34ef37cdce | ||
|
|
5d6b655cb1 | ||
|
|
074a0fa111 | ||
|
|
403d839fca | ||
|
|
4e3098240b | ||
|
|
07fda9bbb3 | ||
|
|
4a7ede11e9 | ||
|
|
482ae4c4f1 | ||
|
|
08fe4cd65f | ||
|
|
5781721bca | ||
|
|
39de0063bf | ||
|
|
202b647234 | ||
|
|
51c163a268 | ||
|
|
6e802c9938 | ||
|
|
9a46104e37 | ||
|
|
655b317c39 | ||
|
|
d3ad7c9d4a | ||
|
|
ece08d3efd | ||
|
|
3493442c2d | ||
|
|
632a79b9e4 | ||
|
|
4a4d85757a | ||
|
|
88a01004b7 | ||
|
|
73230eb35a |
@@ -1,5 +1,15 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.20.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a7ede11e: fix: distinguish files that were not uploaded
|
||||
- 202b64723: feat(nhost-run): add support for one-click-install run services
|
||||
- 074a0fa11: feat(dashboard): add settings toggle to enable/disable antivirus
|
||||
- @nhost/react-apollo@5.0.33
|
||||
- @nhost/nextjs@1.13.35
|
||||
|
||||
## 0.20.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.6",
|
||||
"version": "0.20.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import Link from 'next/link';
|
||||
import { forwardRef, type ForwardedRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import AnnouncementContainer, {
|
||||
@@ -28,7 +27,7 @@ function Announcement(
|
||||
<AnnouncementContainer
|
||||
{...props}
|
||||
ref={ref}
|
||||
className="grid justify-between grid-flow-col gap-4"
|
||||
className="grid grid-flow-col justify-between gap-4"
|
||||
slotProps={{
|
||||
root: {
|
||||
...(slotProps?.root || {}),
|
||||
@@ -39,12 +38,12 @@ function Announcement(
|
||||
<span />
|
||||
|
||||
<div className="flex items-center self-center truncate">
|
||||
<Link href={href}>
|
||||
<Text className="truncate cursor-pointer hover:underline">
|
||||
<a href={href}>
|
||||
<Text className="cursor-pointer truncate hover:underline">
|
||||
{children}
|
||||
</Text>
|
||||
</Link>
|
||||
<ArrowRightIcon className="w-4 h-4 ml-1 text-white" />
|
||||
</a>
|
||||
<ArrowRightIcon className="ml-1 h-4 w-4 text-white" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -52,9 +51,9 @@ function Announcement(
|
||||
onClick={onClose}
|
||||
aria-label="Close announcement"
|
||||
size="small"
|
||||
className="p-1 rounded-sm"
|
||||
className="rounded-sm p-1"
|
||||
>
|
||||
<XIcon className="w-4 h-4 opacity-65" />
|
||||
<XIcon className="opacity-65 h-4 w-4" />
|
||||
</Button>
|
||||
</AnnouncementContainer>
|
||||
);
|
||||
|
||||
@@ -178,6 +178,22 @@ export default function DataGridBody<T extends object>({
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundCellColor = (
|
||||
row: Row<T>,
|
||||
column: DataBrowserGridColumn<T>,
|
||||
) => {
|
||||
// Grey out files not uploaded
|
||||
if (!row.values.isUploaded) {
|
||||
return 'grey.200';
|
||||
}
|
||||
|
||||
if (column.isDisabled) {
|
||||
return 'grey.100';
|
||||
}
|
||||
|
||||
return 'background.paper';
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
@@ -260,9 +276,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
cell={cell}
|
||||
sx={{
|
||||
backgroundColor: column.isDisabled
|
||||
? 'grey.100'
|
||||
: 'background.paper',
|
||||
backgroundColor: getBackgroundCellColor(row, column),
|
||||
color: isCellDisabled ? 'text.secondary' : 'text.primary',
|
||||
}}
|
||||
className={twMerge(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useHostName } from './useHostName';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useHostName() {
|
||||
const [hostName, setHostName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const { port, hostname, protocol } = window.location;
|
||||
setHostName(`${protocol}//${hostname}:${port}`);
|
||||
}, []);
|
||||
|
||||
return hostName;
|
||||
}
|
||||
@@ -3,11 +3,14 @@ import { Form } from '@/components/form/Form';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useHostName } from '@/features/projects/common/hooks/useHostName';
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import {
|
||||
COST_PER_VCPU,
|
||||
@@ -25,6 +28,7 @@ import { StorageFormSection } from '@/features/services/components/ServiceForm/c
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useInsertRunServiceMutation,
|
||||
@@ -109,6 +113,7 @@ export default function ServiceForm({
|
||||
onCancel,
|
||||
location,
|
||||
}: ServiceFormProps) {
|
||||
const hostName = useHostName();
|
||||
const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
|
||||
const [insertRunService] = useInsertRunServiceMutation();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
@@ -146,7 +151,7 @@ export default function ServiceForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const getFormattedConfig = (values: ServiceFormValues) => {
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: values.name,
|
||||
image: {
|
||||
@@ -176,7 +181,13 @@ export default function ServiceForm({
|
||||
})),
|
||||
};
|
||||
|
||||
if (initialData) {
|
||||
return config;
|
||||
};
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const config = getFormattedConfig(values);
|
||||
|
||||
if (serviceID) {
|
||||
// Update service config
|
||||
await replaceRunServiceConfig({
|
||||
variables: {
|
||||
@@ -278,6 +289,16 @@ export default function ServiceForm({
|
||||
return `Approximate cost for ${details}`;
|
||||
};
|
||||
|
||||
const copyConfig = () => {
|
||||
const config = getFormattedConfig(formValues);
|
||||
|
||||
const base64Config = btoa(JSON.stringify(config));
|
||||
|
||||
const link = `${hostName}/run-one-click-install?config=${base64Config}`;
|
||||
|
||||
copy(link, 'Service Config');
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
@@ -427,9 +448,24 @@ export default function ServiceForm({
|
||||
</Alert>
|
||||
)}
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{initialData ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
startIcon={<PlusIcon />}
|
||||
>
|
||||
{serviceID ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
disabled={isSubmitting}
|
||||
onClick={copyConfig}
|
||||
startIcon={<CopyIcon />}
|
||||
>
|
||||
Copy one-click install link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function PortsFormSection() {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
const name = _name && _name.length > 0 ? _name : '[name]';
|
||||
|
||||
return `https://${currentProject.subdomain}-${name}-${port}.svc.${currentProject.region.awsName}.${currentProject.region.domain}`;
|
||||
return `https://${currentProject?.subdomain}-${name}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetStorageSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraStorageAVFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraStorageAVSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetStorageSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { server } = data?.config?.storage?.antivirus || {};
|
||||
|
||||
const form = useForm<HasuraStorageAVFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: !!server,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading AV settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraStorageAVFormValues) {
|
||||
let antivirus = null;
|
||||
|
||||
if (formValues.enabled) {
|
||||
antivirus = {
|
||||
server: 'tcp://run-clamav:3310',
|
||||
};
|
||||
}
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
storage: {
|
||||
antivirus,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Antivirus settings are being updated...`,
|
||||
success: `Antivirus settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update Antivirus settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Antivirus"
|
||||
description="Enable or disable Antivirus."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Antivirus"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraStorageAVSettings';
|
||||
export { default as HasuraStorageAVSettings } from './HasuraStorageAVSettings';
|
||||
@@ -4,6 +4,9 @@ query GetStorageSettings($appId: uuid!) {
|
||||
__typename
|
||||
storage {
|
||||
version
|
||||
antivirus {
|
||||
server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,20 +13,35 @@ import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
|
||||
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
||||
import { ServiceForm } from '@/features/services/components/ServiceForm';
|
||||
import {
|
||||
ServiceForm,
|
||||
type PortTypes,
|
||||
} from '@/features/services/components/ServiceForm';
|
||||
import ServicesList from '@/features/services/components/ServicesList/ServicesList';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
|
||||
export type RunService = Omit<
|
||||
GetRunServicesQuery['app']['runServices'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export type RunServiceConfig = Omit<
|
||||
GetRunServicesQuery['app']['runServices'][0]['config'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function ServicesPage() {
|
||||
const limit = useRef(25);
|
||||
const router = useRouter();
|
||||
const { openDrawer } = useDialog();
|
||||
const { openDrawer, openAlertDialog } = useDialog();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const isPlanFree = currentProject.plan.isFree;
|
||||
|
||||
@@ -66,6 +81,63 @@ export default function ServicesPage() {
|
||||
[data],
|
||||
);
|
||||
|
||||
const checkConfigFromQuery = useCallback(
|
||||
(base64Config: string) => {
|
||||
if (router.query?.config) {
|
||||
try {
|
||||
const decodedConfig = atob(base64Config);
|
||||
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
|
||||
|
||||
openDrawer({
|
||||
title: (
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<CubeIcon className="h-5 w-5" />
|
||||
<Text>Create a new run service</Text>
|
||||
</Box>
|
||||
),
|
||||
component: (
|
||||
<ServiceForm
|
||||
initialData={{
|
||||
...parsedConfig,
|
||||
compute: parsedConfig?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
image: parsedConfig?.image?.image,
|
||||
command: parsedConfig?.command?.join(' '),
|
||||
ports: parsedConfig?.ports.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type as PortTypes,
|
||||
publish: item.publish,
|
||||
})),
|
||||
replicas: parsedConfig?.resources?.replicas,
|
||||
storage: parsedConfig?.resources?.storage,
|
||||
}}
|
||||
onSubmit={refetchServices}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
openAlertDialog({
|
||||
title: 'Configuration not set properly',
|
||||
payload: 'The service configuration was not properly encoded',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[router.query.config, openDrawer, refetchServices, openAlertDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query?.config) {
|
||||
checkConfigFromQuery(router.query?.config as string);
|
||||
}
|
||||
}, [checkConfigFromQuery, router.query]);
|
||||
|
||||
const openCreateServiceDialog = () => {
|
||||
openDrawer({
|
||||
title: (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { StorageServiceVersionSettings } from '@/features/storage/settings/components/HasuraServiceVersionSettings';
|
||||
import { HasuraStorageAVSettings } from '@/features/storage/settings/components/HasuraStorageAVSettings';
|
||||
import { useGetStorageSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function StorageSettingsPage() {
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<StorageServiceVersionSettings />
|
||||
<HasuraStorageAVSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
218
dashboard/src/pages/run-one-click-install.tsx
Normal file
218
dashboard/src/pages/run-one-click-install.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent, ReactElement } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Workspace = Omit<
|
||||
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function SelectWorkspaceAndProject() {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
const { openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const workspaces: Workspace[] = data?.workspaces || [];
|
||||
|
||||
const projects = workspaces.flatMap((workspace) =>
|
||||
workspace.projects.map((project) => ({
|
||||
workspaceName: workspace.name,
|
||||
projectName: project.name,
|
||||
value: `${workspace.slug}/${project.slug}`,
|
||||
isFree: project.plan.isFree,
|
||||
})),
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const checkConfigFromQuery = useCallback(
|
||||
(base64Config: string) => {
|
||||
try {
|
||||
JSON.parse(atob(base64Config));
|
||||
} catch (error) {
|
||||
openAlertDialog({
|
||||
title: 'Configuration not set properly',
|
||||
payload:
|
||||
'Either the link is wrong or the configuration is not properly encoded',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
onPrimaryAction: async () => {
|
||||
await router.push('/');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[openAlertDialog, router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
checkConfigFromQuery(router.query?.config as string);
|
||||
}, [checkConfigFromQuery, router.query]);
|
||||
|
||||
const goToServices = async (project: {
|
||||
workspaceName: string;
|
||||
projectName: string;
|
||||
value: string;
|
||||
isFree: boolean;
|
||||
}) => {
|
||||
if (!project) {
|
||||
openAlertDialog({
|
||||
title: 'Please select a workspace and a project',
|
||||
payload:
|
||||
'You must select a workspace and a project before proceeding to create the run service',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.isFree) {
|
||||
openAlertDialog({
|
||||
title: 'The project must have a pro plan',
|
||||
payload: 'Creating run services is only availabel for pro projects',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push({
|
||||
pathname: `/${project.value}/services`,
|
||||
// Keep the same query params that got us here
|
||||
query: router.query,
|
||||
});
|
||||
};
|
||||
|
||||
const projectsToDisplay = filter
|
||||
? projects.filter((project) =>
|
||||
project.projectName.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: projects;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading workspaces and projects..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="mx-auto grid max-w-[760px] grid-flow-row gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
New Run Service
|
||||
</Text>
|
||||
|
||||
<InfoCard
|
||||
title="Please select the workspace and the project where you want to create the service"
|
||||
disableCopy
|
||||
value=""
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{projectsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{projectsToDisplay.map((project, index) => (
|
||||
<Fragment key={project.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToServices(project)}
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={project.projectName}
|
||||
secondary={`${project.workspaceName} / ${project.projectName}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < projects.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="New Run Service">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
29
dashboard/src/utils/__generated__/graphql.ts
generated
29
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -1805,6 +1805,7 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
|
||||
/** Configuration for storage service */
|
||||
export type ConfigStorage = {
|
||||
__typename?: 'ConfigStorage';
|
||||
antivirus?: Maybe<ConfigStorageAntivirus>;
|
||||
/** Resources for the service */
|
||||
resources?: Maybe<ConfigResources>;
|
||||
/**
|
||||
@@ -1818,20 +1819,43 @@ export type ConfigStorage = {
|
||||
version?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirus = {
|
||||
__typename?: 'ConfigStorageAntivirus';
|
||||
server?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigStorageAntivirusComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigStorageAntivirusComparisonExp>>;
|
||||
server?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusInsertInput = {
|
||||
server?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusUpdateInput = {
|
||||
server?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigStorageComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigStorageComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigStorageComparisonExp>>;
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
|
||||
resources?: InputMaybe<ConfigResourcesComparisonExp>;
|
||||
version?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigStorageInsertInput = {
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusInsertInput>;
|
||||
resources?: InputMaybe<ConfigResourcesInsertInput>;
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageUpdateInput = {
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusUpdateInput>;
|
||||
resources?: InputMaybe<ConfigResourcesUpdateInput>;
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@@ -19750,7 +19774,7 @@ export type GetStorageSettingsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetStorageSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', storage?: { __typename?: 'ConfigStorage', version?: string | null } | null } | null };
|
||||
export type GetStorageSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', storage?: { __typename?: 'ConfigStorage', version?: string | null, antivirus?: { __typename?: 'ConfigStorageAntivirus', server?: string | null } | null } | null } | null };
|
||||
|
||||
export type DeleteApplicationMutationVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -21063,6 +21087,9 @@ export const GetStorageSettingsDocument = gql`
|
||||
__typename
|
||||
storage {
|
||||
version
|
||||
antivirus {
|
||||
server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ httpPoolSize = 100
|
||||
version = 16
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
version = '0.21.2'
|
||||
|
||||
[auth.redirections]
|
||||
clientUrl = 'http://localhost:3000'
|
||||
clientUrl = 'https://react-apollo.example.nhost.io/'
|
||||
|
||||
[auth.signUp]
|
||||
enabled = true
|
||||
@@ -94,13 +94,17 @@ enabled = false
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.github]
|
||||
enabled = false
|
||||
enabled = true
|
||||
clientId = '{{ secrets.GITHUB_CLIENT_ID }}'
|
||||
clientSecret = '{{ secrets.GITHUB_CLIENT_SECRET }}'
|
||||
|
||||
[auth.method.oauth.gitlab]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.google]
|
||||
enabled = false
|
||||
enabled = true
|
||||
clientId = '{{ secrets.GOOGLE_CLIENT_ID }}'
|
||||
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
|
||||
|
||||
[auth.method.oauth.linkedin]
|
||||
enabled = false
|
||||
@@ -124,7 +128,11 @@ enabled = false
|
||||
enabled = false
|
||||
|
||||
[auth.method.webauthn]
|
||||
enabled = false
|
||||
enabled = true
|
||||
|
||||
[auth.method.webauthn.relyingParty]
|
||||
name = 'apollo-example'
|
||||
origins = ['https://react-apollo.example.nhost.io']
|
||||
|
||||
[auth.method.webauthn.attestation]
|
||||
timeout = 60000
|
||||
|
||||
12
examples/react-apollo/nhost/overlays/local.json
Normal file
12
examples/react-apollo/nhost/overlays/local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/auth/method/webauthn/relyingParty/origins/0",
|
||||
"value": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/auth/redirections/clientUrl",
|
||||
"value": "http://localhost:3000"
|
||||
}
|
||||
]
|
||||
@@ -8,15 +8,15 @@
|
||||
outputs = { self, nixpkgs, flake-utils, nix-filter }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
version = "v1.1.0";
|
||||
version = "v1.5.2";
|
||||
dist = {
|
||||
aarch64-darwin = rec {
|
||||
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-darwin-arm64.tar.gz";
|
||||
sha256 = "sha256-tF40CEkA357yzg2Gmc9ubjHJ5FlI9qQTdVdWNY/+f+Y=";
|
||||
sha256 = "0rakx9lpbj5m3jfra5r2iw065x800i951843l3mxbwk9m1hzm185";
|
||||
};
|
||||
x86_64-linux = rec {
|
||||
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-linux-amd64.tar.gz";
|
||||
sha256 = "sha256-KLv06dI7A+5KGJ5F8xM1qC+oqHRmJ4kMaifLvaTFqak=";
|
||||
sha256 = "19x83fpvazwzpnrxi0qh6mh14wwhlw77qd7vvc3633dxa5hdigc4";
|
||||
};
|
||||
};
|
||||
overlays = [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 5.2.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@2.2.14
|
||||
|
||||
## 5.2.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "5.2.15",
|
||||
"version": "5.2.16",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/react-apollo
|
||||
|
||||
## 5.0.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@5.2.16
|
||||
- @nhost/react@2.0.29
|
||||
|
||||
## 5.0.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "5.0.32",
|
||||
"version": "5.0.33",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/react-urql
|
||||
|
||||
## 2.0.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@2.0.29
|
||||
|
||||
## 2.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-urql",
|
||||
"version": "2.0.29",
|
||||
"version": "2.0.30",
|
||||
"description": "Nhost React URQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/hasura-storage-js
|
||||
|
||||
## 2.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 39de0063b: fix(hasura-storage-js): fix upload response status code check
|
||||
|
||||
## 2.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-storage-js",
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"description": "Hasura-storage client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -87,10 +87,14 @@ export const fetchUpload = async (
|
||||
xhr.responseType = 'json'
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status < 200 && xhr.status >= 300) {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
return resolve({
|
||||
fileMetadata: null,
|
||||
error: { error: xhr.statusText, message: xhr.statusText, status: xhr.status }
|
||||
error: {
|
||||
error: xhr.response?.error ?? xhr.response,
|
||||
message: xhr.response?.error?.message ?? xhr.response,
|
||||
status: xhr.status
|
||||
}
|
||||
})
|
||||
}
|
||||
return resolve({ fileMetadata: xhr.response, error: null })
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/nextjs
|
||||
|
||||
## 1.13.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@2.0.29
|
||||
|
||||
## 1.13.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nextjs",
|
||||
"version": "1.13.34",
|
||||
"version": "1.13.35",
|
||||
"description": "Nhost NextJS library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/nhost-js
|
||||
|
||||
## 2.2.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [39de0063b]
|
||||
- @nhost/hasura-storage-js@2.2.3
|
||||
|
||||
## 2.2.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/nhost-js",
|
||||
"version": "2.2.13",
|
||||
"version": "2.2.14",
|
||||
"description": "Nhost JavaScript SDK",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/react
|
||||
|
||||
## 2.0.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@2.2.14
|
||||
|
||||
## 2.0.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react",
|
||||
"version": "2.0.28",
|
||||
"version": "2.0.29",
|
||||
"description": "Nhost React library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/vue
|
||||
|
||||
## 1.13.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@2.2.14
|
||||
|
||||
## 1.13.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/vue",
|
||||
"version": "1.13.33",
|
||||
"version": "1.13.34",
|
||||
"description": "Nhost Vue library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
Reference in New Issue
Block a user