Compare commits

..

26 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
d9fd1a54a5 Merge pull request #2192 from nhost/changeset-release/main
chore: update versions
2023-08-23 11:19:48 +01:00
github-actions[bot]
a19b85c8ac chore: update versions 2023-08-23 09:45:31 +00:00
Hassan Ben Jobrane
4e1aaca0ee Merge pull request #2194 from nhost/feat/toggle-av
feat: toggle av
2023-08-23 10:42:20 +01:00
Hassan Ben Jobrane
34ef37cdce Merge pull request #2190 from dddenis/fix/storage-upload-status-error
fix(hasura-storage-js): fix upload response status code check
2023-08-23 10:40:29 +01:00
Hassan Ben Jobrane
5d6b655cb1 fix: make sure AV turns off correctly 2023-08-23 01:40:01 +01:00
Hassan Ben Jobrane
074a0fa111 chore: add changeset 2023-08-22 18:32:34 +01:00
Hassan Ben Jobrane
403d839fca chore: cleanup 2023-08-22 18:30:26 +01:00
Hassan Ben Jobrane
4e3098240b feat(settings): add toggle av settings 2023-08-22 18:28:27 +01:00
Hassan Ben Jobrane
07fda9bbb3 Merge pull request #2193 from nhost/fix/distinguish-not-uploaded-files
fix: grey out not uploaded files
2023-08-22 13:11:52 +01:00
Hassan Ben Jobrane
4a7ede11e9 chore: add changeset 2023-08-22 11:33:42 +01:00
Hassan Ben Jobrane
482ae4c4f1 fix: grey not uploaded files 2023-08-22 11:25:23 +01:00
Denis Goncharenko
08fe4cd65f fix(hasura-storage-js): update upload response error details 2023-08-22 11:34:41 +02:00
Hassan Ben Jobrane
5781721bca Merge pull request #2188 from nhost/feat/one-click-run-service
feat: add support for template run services
2023-08-22 10:18:54 +01:00
Denis Goncharenko
39de0063bf chore: add changeset 2023-08-21 20:49:56 +02:00
Hassan Ben Jobrane
202b647234 chore: add changeset 2023-08-21 15:45:43 +01:00
Hassan Ben Jobrane
51c163a268 fix: copy complete link to config 2023-08-21 15:43:46 +01:00
Hassan Ben Jobrane
6e802c9938 fix: handle the case where the config is not set in the URL 2023-08-21 13:38:38 +01:00
Hassan Ben Jobrane
9a46104e37 feat: replace project selector with a searchable list 2023-08-21 13:26:27 +01:00
Hassan Ben Jobrane
655b317c39 fix: keep image field when copying config to clipboard 2023-08-21 13:25:57 +01:00
Hassan Ben Jobrane
d3ad7c9d4a fix: handle error when navigating back when service form is still open 2023-08-21 13:25:01 +01:00
Hassan Ben Jobrane
ece08d3efd feat: add ability to copy current service config from the editor 2023-08-21 10:47:54 +01:00
Hassan Ben Jobrane
3493442c2d fix: fix command import when value is null 2023-08-21 10:47:26 +01:00
Denis Goncharenko
632a79b9e4 fix(hasura-storage-js): fix upload response status code check 2023-08-20 14:15:24 +02:00
Hassan Ben Jobrane
4a4d85757a fix: use serviceId to determine whether to create or update service 2023-08-19 19:33:45 +01:00
Hassan Ben Jobrane
88a01004b7 feat: parse base64 encoded config from query param 2023-08-19 18:50:46 +01:00
David Barroso
73230eb35a chore: fix and deploy react example (#2183) 2023-08-19 07:57:42 +02:00
35 changed files with 631 additions and 40 deletions

View File

@@ -1,5 +1,15 @@
# @nhost/dashboard # @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 ## 0.20.6
### Patch Changes ### Patch Changes

View File

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

View File

@@ -2,7 +2,6 @@ import { Button } from '@/components/ui/v2/Button';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon'; import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon'; import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import Link from 'next/link';
import { forwardRef, type ForwardedRef } from 'react'; import { forwardRef, type ForwardedRef } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import AnnouncementContainer, { import AnnouncementContainer, {
@@ -28,7 +27,7 @@ function Announcement(
<AnnouncementContainer <AnnouncementContainer
{...props} {...props}
ref={ref} ref={ref}
className="grid justify-between grid-flow-col gap-4" className="grid grid-flow-col justify-between gap-4"
slotProps={{ slotProps={{
root: { root: {
...(slotProps?.root || {}), ...(slotProps?.root || {}),
@@ -39,12 +38,12 @@ function Announcement(
<span /> <span />
<div className="flex items-center self-center truncate"> <div className="flex items-center self-center truncate">
<Link href={href}> <a href={href}>
<Text className="truncate cursor-pointer hover:underline"> <Text className="cursor-pointer truncate hover:underline">
{children} {children}
</Text> </Text>
</Link> </a>
<ArrowRightIcon className="w-4 h-4 ml-1 text-white" /> <ArrowRightIcon className="ml-1 h-4 w-4 text-white" />
</div> </div>
<Button <Button
@@ -52,9 +51,9 @@ function Announcement(
onClick={onClose} onClick={onClose}
aria-label="Close announcement" aria-label="Close announcement"
size="small" 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> </Button>
</AnnouncementContainer> </AnnouncementContainer>
); );

View File

@@ -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 ( return (
<div {...getTableBodyProps()} ref={bodyRef} {...props}> <div {...getTableBodyProps()} ref={bodyRef} {...props}>
{rows.length === 0 && !loading && ( {rows.length === 0 && !loading && (
@@ -260,9 +276,7 @@ export default function DataGridBody<T extends object>({
})} })}
cell={cell} cell={cell}
sx={{ sx={{
backgroundColor: column.isDisabled backgroundColor: getBackgroundCellColor(row, column),
? 'grey.100'
: 'background.paper',
color: isCellDisabled ? 'text.secondary' : 'text.primary', color: isCellDisabled ? 'text.secondary' : 'text.primary',
}} }}
className={twMerge( className={twMerge(

View File

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

View File

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

View File

@@ -3,11 +3,14 @@ import { Form } from '@/components/form/Form';
import { Alert } from '@/components/ui/v2/Alert'; import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon'; import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip'; import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; 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 { InfoCard } from '@/features/projects/overview/components/InfoCard';
import { import {
COST_PER_VCPU, COST_PER_VCPU,
@@ -25,6 +28,7 @@ import { StorageFormSection } from '@/features/services/components/ServiceForm/c
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common'; import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { import {
useInsertRunServiceConfigMutation, useInsertRunServiceConfigMutation,
useInsertRunServiceMutation, useInsertRunServiceMutation,
@@ -109,6 +113,7 @@ export default function ServiceForm({
onCancel, onCancel,
location, location,
}: ServiceFormProps) { }: ServiceFormProps) {
const hostName = useHostName();
const { onDirtyStateChange, openDialog, closeDialog } = useDialog(); const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
const [insertRunService] = useInsertRunServiceMutation(); const [insertRunService] = useInsertRunServiceMutation();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
@@ -146,7 +151,7 @@ export default function ServiceForm({
onDirtyStateChange(isDirty, location); onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
const createOrUpdateService = async (values: ServiceFormValues) => { const getFormattedConfig = (values: ServiceFormValues) => {
const config: ConfigRunServiceConfigInsertInput = { const config: ConfigRunServiceConfigInsertInput = {
name: values.name, name: values.name,
image: { 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 // Update service config
await replaceRunServiceConfig({ await replaceRunServiceConfig({
variables: { variables: {
@@ -278,6 +289,16 @@ export default function ServiceForm({
return `Approximate cost for ${details}`; 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 ( return (
<FormProvider {...form}> <FormProvider {...form}>
<Form <Form
@@ -427,9 +448,24 @@ export default function ServiceForm({
</Alert> </Alert>
)} )}
<div className="grid grid-flow-row gap-2"> <div className="grid grid-flow-row gap-2">
<Button type="submit" disabled={isSubmitting}> <div className="grid grid-cols-2 gap-2">
{initialData ? 'Update' : 'Create'} <Button
</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}> <Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel Cancel

View File

@@ -45,7 +45,7 @@ export default function PortsFormSection() {
const port = Number(_port) > 0 ? Number(_port) : '[port]'; const port = Number(_port) > 0 ? Number(_port) : '[port]';
const name = _name && _name.length > 0 ? _name : '[name]'; 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 ( return (

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ query GetStorageSettings($appId: uuid!) {
__typename __typename
storage { storage {
version version
antivirus {
server
}
} }
} }
} }

View File

@@ -13,20 +13,35 @@ import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql'; import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification'; 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 ServicesList from '@/features/services/components/ServicesList/ServicesList';
import { useRouter } from 'next/router'; 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< export type RunService = Omit<
GetRunServicesQuery['app']['runServices'][0], GetRunServicesQuery['app']['runServices'][0],
'__typename' '__typename'
>; >;
export type RunServiceConfig = Omit<
GetRunServicesQuery['app']['runServices'][0]['config'],
'__typename'
>;
export default function ServicesPage() { export default function ServicesPage() {
const limit = useRef(25); const limit = useRef(25);
const router = useRouter(); const router = useRouter();
const { openDrawer } = useDialog(); const { openDrawer, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject.plan.isFree; const isPlanFree = currentProject.plan.isFree;
@@ -66,6 +81,63 @@ export default function ServicesPage() {
[data], [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 = () => { const openCreateServiceDialog = () => {
openDrawer({ openDrawer({
title: ( title: (

View File

@@ -3,6 +3,7 @@ import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { StorageServiceVersionSettings } from '@/features/storage/settings/components/HasuraServiceVersionSettings'; import { StorageServiceVersionSettings } from '@/features/storage/settings/components/HasuraServiceVersionSettings';
import { HasuraStorageAVSettings } from '@/features/storage/settings/components/HasuraStorageAVSettings';
import { useGetStorageSettingsQuery } from '@/utils/__generated__/graphql'; import { useGetStorageSettingsQuery } from '@/utils/__generated__/graphql';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
@@ -34,6 +35,7 @@ export default function StorageSettingsPage() {
rootClassName="bg-transparent" rootClassName="bg-transparent"
> >
<StorageServiceVersionSettings /> <StorageServiceVersionSettings />
<HasuraStorageAVSettings />
</Container> </Container>
); );
} }

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

View File

@@ -1805,6 +1805,7 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
/** Configuration for storage service */ /** Configuration for storage service */
export type ConfigStorage = { export type ConfigStorage = {
__typename?: 'ConfigStorage'; __typename?: 'ConfigStorage';
antivirus?: Maybe<ConfigStorageAntivirus>;
/** Resources for the service */ /** Resources for the service */
resources?: Maybe<ConfigResources>; resources?: Maybe<ConfigResources>;
/** /**
@@ -1818,20 +1819,43 @@ export type ConfigStorage = {
version?: Maybe<Scalars['String']>; 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 = { export type ConfigStorageComparisonExp = {
_and?: InputMaybe<Array<ConfigStorageComparisonExp>>; _and?: InputMaybe<Array<ConfigStorageComparisonExp>>;
_not?: InputMaybe<ConfigStorageComparisonExp>; _not?: InputMaybe<ConfigStorageComparisonExp>;
_or?: InputMaybe<Array<ConfigStorageComparisonExp>>; _or?: InputMaybe<Array<ConfigStorageComparisonExp>>;
antivirus?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>; resources?: InputMaybe<ConfigResourcesComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>; version?: InputMaybe<ConfigStringComparisonExp>;
}; };
export type ConfigStorageInsertInput = { export type ConfigStorageInsertInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>; resources?: InputMaybe<ConfigResourcesInsertInput>;
version?: InputMaybe<Scalars['String']>; version?: InputMaybe<Scalars['String']>;
}; };
export type ConfigStorageUpdateInput = { export type ConfigStorageUpdateInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>; resources?: InputMaybe<ConfigResourcesUpdateInput>;
version?: InputMaybe<Scalars['String']>; 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<{ export type DeleteApplicationMutationVariables = Exact<{
appId: Scalars['uuid']; appId: Scalars['uuid'];
@@ -21063,6 +21087,9 @@ export const GetStorageSettingsDocument = gql`
__typename __typename
storage { storage {
version version
antivirus {
server
}
} }
} }
} }

View File

@@ -28,10 +28,10 @@ httpPoolSize = 100
version = 16 version = 16
[auth] [auth]
version = '0.20.2' version = '0.21.2'
[auth.redirections] [auth.redirections]
clientUrl = 'http://localhost:3000' clientUrl = 'https://react-apollo.example.nhost.io/'
[auth.signUp] [auth.signUp]
enabled = true enabled = true
@@ -94,13 +94,17 @@ enabled = false
enabled = false enabled = false
[auth.method.oauth.github] [auth.method.oauth.github]
enabled = false enabled = true
clientId = '{{ secrets.GITHUB_CLIENT_ID }}'
clientSecret = '{{ secrets.GITHUB_CLIENT_SECRET }}'
[auth.method.oauth.gitlab] [auth.method.oauth.gitlab]
enabled = false enabled = false
[auth.method.oauth.google] [auth.method.oauth.google]
enabled = false enabled = true
clientId = '{{ secrets.GOOGLE_CLIENT_ID }}'
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
[auth.method.oauth.linkedin] [auth.method.oauth.linkedin]
enabled = false enabled = false
@@ -124,7 +128,11 @@ enabled = false
enabled = false enabled = false
[auth.method.webauthn] [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] [auth.method.webauthn.attestation]
timeout = 60000 timeout = 60000

View 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"
}
]

View File

@@ -8,15 +8,15 @@
outputs = { self, nixpkgs, flake-utils, nix-filter }: outputs = { self, nixpkgs, flake-utils, nix-filter }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
version = "v1.1.0"; version = "v1.5.2";
dist = { dist = {
aarch64-darwin = rec { aarch64-darwin = rec {
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-darwin-arm64.tar.gz"; 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 { x86_64-linux = rec {
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-linux-amd64.tar.gz"; url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-linux-amd64.tar.gz";
sha256 = "sha256-KLv06dI7A+5KGJ5F8xM1qC+oqHRmJ4kMaifLvaTFqak="; sha256 = "19x83fpvazwzpnrxi0qh6mh14wwhlw77qd7vvc3633dxa5hdigc4";
}; };
}; };
overlays = [ overlays = [

View File

@@ -1,5 +1,11 @@
# @nhost/apollo # @nhost/apollo
## 5.2.16
### Patch Changes
- @nhost/nhost-js@2.2.14
## 5.2.15 ## 5.2.15
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/apollo", "name": "@nhost/apollo",
"version": "5.2.15", "version": "5.2.16",
"description": "Nhost Apollo Client library", "description": "Nhost Apollo Client library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/react-apollo # @nhost/react-apollo
## 5.0.33
### Patch Changes
- @nhost/apollo@5.2.16
- @nhost/react@2.0.29
## 5.0.32 ## 5.0.32
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-apollo", "name": "@nhost/react-apollo",
"version": "5.0.32", "version": "5.0.33",
"description": "Nhost React Apollo client", "description": "Nhost React Apollo client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/react-urql # @nhost/react-urql
## 2.0.30
### Patch Changes
- @nhost/react@2.0.29
## 2.0.29 ## 2.0.29
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-urql", "name": "@nhost/react-urql",
"version": "2.0.29", "version": "2.0.30",
"description": "Nhost React URQL client", "description": "Nhost React URQL client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/hasura-storage-js # @nhost/hasura-storage-js
## 2.2.3
### Patch Changes
- 39de0063b: fix(hasura-storage-js): fix upload response status code check
## 2.2.2 ## 2.2.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/hasura-storage-js", "name": "@nhost/hasura-storage-js",
"version": "2.2.2", "version": "2.2.3",
"description": "Hasura-storage client", "description": "Hasura-storage client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -87,10 +87,14 @@ export const fetchUpload = async (
xhr.responseType = 'json' xhr.responseType = 'json'
xhr.onload = () => { xhr.onload = () => {
if (xhr.status < 200 && xhr.status >= 300) { if (xhr.status < 200 || xhr.status >= 300) {
return resolve({ return resolve({
fileMetadata: null, 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 }) return resolve({ fileMetadata: xhr.response, error: null })

View File

@@ -1,5 +1,11 @@
# @nhost/nextjs # @nhost/nextjs
## 1.13.35
### Patch Changes
- @nhost/react@2.0.29
## 1.13.34 ## 1.13.34
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/nextjs", "name": "@nhost/nextjs",
"version": "1.13.34", "version": "1.13.35",
"description": "Nhost NextJS library", "description": "Nhost NextJS library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/nhost-js # @nhost/nhost-js
## 2.2.14
### Patch Changes
- Updated dependencies [39de0063b]
- @nhost/hasura-storage-js@2.2.3
## 2.2.13 ## 2.2.13
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/nhost-js", "name": "@nhost/nhost-js",
"version": "2.2.13", "version": "2.2.14",
"description": "Nhost JavaScript SDK", "description": "Nhost JavaScript SDK",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/react # @nhost/react
## 2.0.29
### Patch Changes
- @nhost/nhost-js@2.2.14
## 2.0.28 ## 2.0.28
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react", "name": "@nhost/react",
"version": "2.0.28", "version": "2.0.29",
"description": "Nhost React library", "description": "Nhost React library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/vue # @nhost/vue
## 1.13.34
### Patch Changes
- @nhost/nhost-js@2.2.14
## 1.13.33 ## 1.13.33
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/vue", "name": "@nhost/vue",
"version": "1.13.33", "version": "1.13.34",
"description": "Nhost Vue library", "description": "Nhost Vue library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [