Compare commits

..

4 Commits

Author SHA1 Message Date
Szilárd Dóró
7e113bfb1f Merge pull request #1253 from nhost/changeset-release/main
chore: update versions
2022-11-30 14:11:45 +01:00
github-actions[bot]
f1d358d77c chore: update versions 2022-11-30 11:16:45 +00:00
Szilárd Dóró
d558ef9ecf Merge pull request #1249 from nhost/chore/dashboard-cleanup
chore(dashboard): cleanup unused files
2022-11-30 12:08:39 +01:00
Szilárd Dóró
44f13f6240 chore(dashboard): cleanup unused files 2022-11-29 20:29:25 +01:00
32 changed files with 11 additions and 1419 deletions

View File

@@ -1,5 +1,11 @@
# @nhost/dashboard
## 0.7.2
### Patch Changes
- 44f13f62: chore(dashboard): cleanup unused files
## 0.7.1
### Patch Changes

View File

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

View File

@@ -1,107 +0,0 @@
import { useWorkspaceContext } from '@/context/workspace-context';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { inputErrorMessages } from '@/utils/getErrorMessage';
import { slugifyString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import { useUpdateApplicationMutation } from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
export function ChangeApplicationName({ close }: any) {
const [updateAppName, { client }] = useUpdateApplicationMutation({});
const { workspaceContext } = useWorkspaceContext();
const [name, setName] = useState(workspaceContext.appName);
const [applicationError, setApplicationError] = useState<any>('');
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const router = useRouter();
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const slug = slugifyString(name);
if (slug.length < 4 || slug.length > 32) {
setApplicationError('Slug should be within 4 and 32 characters.');
return;
}
if (!inputErrorMessages(name, setName, setApplicationError, 'project')) {
return;
}
try {
await updateAppName({
variables: {
appId: currentApplication.id,
app: {
name,
slug,
},
},
});
triggerToast('Project name changed');
} catch (error) {
await discordAnnounce(
`Error trying to delete project: ${currentApplication.name}`,
);
}
await updateOwnCache(client);
await router.push(`/${currentWorkspace.slug}/${slug}`);
}
return (
<div className="w-modal px-6 py-6 text-left">
<div className="flex flex-col">
<Text variant="h3" component="h2">
Change Project Name
</Text>
<form onSubmit={handleSubmit}>
<div className="mt-4 grid grid-flow-row gap-2">
<Input
label="New Project Name"
id="projectName"
value={name}
onChange={(e) => {
setName(e.target.value);
setApplicationError('');
}}
fullWidth
autoFocus
helperText={`https://app.nhost.io/${
currentWorkspace.slug
}/${slugifyString(name)}`}
/>
{applicationError && (
<Alert severity="error">{applicationError}</Alert>
)}
</div>
<div className="mt-4 grid grid-flow-row gap-2">
<Button type="submit" disabled={applicationError}>
Save
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
onClick={close}
>
Close
</Button>
</div>
</form>
</div>
</div>
);
}
export default ChangeApplicationName;

View File

@@ -1,327 +0,0 @@
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
import TwilioIcon from '@/components/icons/TwilioIcon';
import {
useGetSmsSettingsQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Button, Input } from '@/ui';
import { Alert } from '@/ui/Alert';
import DelayedLoading from '@/ui/DelayedLoading';
import { Text } from '@/ui/Text';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import { useApolloClient } from '@apollo/client';
import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import {
Controller,
FormProvider,
useForm,
useFormContext,
} from 'react-hook-form';
import toast from 'react-hot-toast';
export function EditSMSSettingsForm({
close,
isAlreadyEnabled,
}: {
close: () => void;
isAlreadyEnabled: boolean;
}) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const {
handleSubmit,
watch,
formState: { isSubmitting, errors },
} = useFormContext<EditSMSSettingsFormData>();
const { control } = useFormContext<EditSMSSettingsFormData>();
const [updateApp] = useUpdateAppMutation();
let toastId: string;
const client = useApolloClient();
const isNotCompleted =
!watch('accountSID') ||
!watch('authToken') ||
!watch('messagingServiceSID');
const handleEditSMSSettings = async (data: EditSMSSettingsFormData) => {
try {
toastId = showLoadingToast('Updating SMS settings...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authSmsTwilioAccountSid: data.accountSID,
authSmsTwilioAuthToken: data.authToken,
authSmsTwilioMessagingServiceId: data.messagingServiceSID,
authSmsPasswordlessEnabled: true,
},
},
});
await client.refetchQueries({ include: ['getSMSSettings'] });
toast.remove(toastId);
triggerToast('SMS settings updated successfully.');
close();
} catch (error) {
if (toastId) {
toast.remove(toastId);
}
throw error;
}
};
return (
<form
onSubmit={handleSubmit(handleEditSMSSettings)}
className="flex w-full flex-col pb-1"
autoComplete="off"
>
{errors &&
Object.entries(errors).map(([type, error]) => (
<Alert key={type} className="mb-4" severity="error">
{error.message}
</Alert>
))}
<div>
<div className="flex flex-row place-content-between border-t border-b px-2 py-2.5">
<div className="flex w-full flex-row">
<Text
color="greyscaleDark"
className="self-center font-medium"
size="normal"
>
Account SID
</Text>
</div>
<div className="flex w-full">
<Controller
name="accountSID"
control={control}
rules={{
required: true,
pattern: {
value: /^[a-zA-Z0-9-_]+$/,
message:
'The Account SID must contain only letters, hyphens, and numbers.',
},
}}
render={({ field }) => (
<Input
{...field}
id="accountSID"
placeholder="Account SID"
required
value={field.value || ''}
onChange={(value: string) => {
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
// prevent the user from entering invalid characters
return;
}
field.onChange(value);
}}
/>
)}
/>
</div>
</div>
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
<div className="flex w-full flex-row">
<Text
color="greyscaleDark"
className="self-center font-medium"
size="normal"
>
Auth Token
</Text>
</div>
<div className="flex w-full">
<Controller
name="authToken"
control={control}
rules={{
required: true,
pattern: {
value: /^[a-zA-Z0-9-_]+$/,
message:
'The Auth Token must contain only letters, hyphens, and numbers.',
},
}}
render={({ field }) => (
<Input
{...field}
id="authToken"
placeholder="Auth Token"
required
value={field.value || ''}
onChange={(value: string) => {
if (value && !/^[a-zA-Z0-9-_/.]+$/gi.test(value)) {
return;
}
field.onChange(value);
}}
/>
)}
/>
</div>
</div>
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
<div className="flex w-full flex-row">
<Text
color="greyscaleDark"
className="self-center font-medium"
size="normal"
>
Messaging Service SID
</Text>
</div>
<div className="flex w-full">
<Controller
name="messagingServiceSID"
control={control}
rules={{
required: true,
pattern: {
value: /^[+a-zA-Z0-9-_/.]+$/,
message:
'The Messaging Service SID must either be a valid phone number or contain only letters, hyphens, and numbers.',
},
}}
render={({ field }) => (
<Input
{...field}
id="messagingServiceSID"
required
placeholder="Messaging Service SID"
value={field.value || ''}
onChange={(value: string) => {
if (value && !/^[+a-zA-Z0-9-_/.]+$/gi.test(value)) {
return;
}
field.onChange(value);
}}
/>
)}
/>
</div>
</div>
</div>
<div className="flex flex-col">
<Button
variant="primary"
type="submit"
className="text-grayscaleDark mt-2 border text-sm+ font-normal"
loading={isSubmitting}
disabled={isSubmitting || isNotCompleted}
>
{isAlreadyEnabled ? 'Update SMS Settings' : 'Enable SMS'}
</Button>
</div>
</form>
);
}
export function EditSMSSettingsModal({
close,
isAlreadyEnabled,
}: {
close: () => void;
isAlreadyEnabled: boolean;
}) {
return (
<div className="w-modal px-6 py-4 text-left">
<div className="flex flex-col">
<div className="mx-auto mt-2.5">
<TwilioIcon className=" text-greyscaleDark" />
</div>
<Text
variant="subHeading"
color="greyscaleDark"
size="large"
className="mt-3 text-center"
>
Set up Twilio SMS Service
</Text>
<Text
variant="body"
color="greyscaleDark"
size="small"
className="mt-0.5 mb-6 text-center font-normal"
>
SMS messages are sent through Twilio. Create an account and a
messaging service at https://console.twilio.com.
</Text>
<div>
<ErrorBoundary fallbackRender={ErrorBoundaryFallback}>
<EditSMSSettingsForm
close={close}
isAlreadyEnabled={isAlreadyEnabled}
/>
</ErrorBoundary>
</div>
</div>
</div>
);
}
export interface EditSMSSettingsProps {
close: () => void;
}
export interface EditSMSSettingsFormData {
accountSID: string;
authToken: string;
messagingServiceSID: string;
}
export function EditSMSSettings({ close }: EditSMSSettingsProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<EditSMSSettingsFormData>({
reValidateMode: 'onSubmit',
defaultValues: {
accountSID: '',
authToken: '',
messagingServiceSID: '',
},
});
const { data, loading, error } = useGetSmsSettingsQuery({
variables: {
id: currentApplication.id,
},
});
useEffect(() => {
if (!data) {
return;
}
form.setValue('accountSID', data.app.authSmsTwilioAccountSid);
form.setValue('authToken', data.app.authSmsTwilioAuthToken);
form.setValue(
'messagingServiceSID',
data.app.authSmsTwilioMessagingServiceId,
);
}, [data, form]);
if (loading) {
return <DelayedLoading delay={500} />;
}
if (error) {
throw error;
}
return (
<FormProvider {...form}>
<EditSMSSettingsModal
close={close}
isAlreadyEnabled={data.app.authSmsPasswordlessEnabled}
/>
</FormProvider>
);
}

View File

@@ -1,134 +0,0 @@
import {
useGetSmsSettingsQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Text, Toggle } from '@/ui';
import DelayedLoading from '@/ui/DelayedLoading';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import { useApolloClient } from '@apollo/client';
import clsx from 'clsx';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useGetSmsSettingsQuery({
variables: {
id: currentApplication.id,
},
});
const [enableSMSLoginMethod, setEnableSMSLoginMethod] = useState(false);
const client = useApolloClient();
let toastId: string;
useEffect(() => {
if (!data) {
return;
}
setEnableSMSLoginMethod(data.app.authSmsPasswordlessEnabled);
}, [data]);
if (loading) {
return <DelayedLoading delay={500} />;
}
if (error) {
throw error;
}
const handleDisable = async () => {
try {
toastId = showLoadingToast('Disabling SMS login...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authSmsPasswordlessEnabled: false,
},
},
});
setEnableSMSLoginMethod(false);
await client.refetchQueries({ include: ['getSMSSettings'] });
toast.remove(toastId);
triggerToast('Passwordless SMS disabled.');
} catch (updateError) {
if (toastId) {
toast.remove(toastId);
}
throw updateError;
}
};
return (
<div className="mt-20">
<div className="mx-auto font-display">
<div className="flex flex-col place-content-between">
<div className="">
<div className="flex flex-row place-content-between">
<div className="relative flex flex-row">
<Image
src="/assets/SMS.svg"
width={24}
height={24}
alt="Phone Number (SMS)"
/>
<Text
variant="body"
size="large"
className="ml-2 font-medium"
color="greyscaleDark"
>
Phone Number (SMS)
</Text>
<div>
<button
type="button"
className={clsx(
'ml-2 align-bottom text-sm- font-medium text-blue transition-opacity duration-300',
!enableSMSLoginMethod && 'invisible opacity-0',
enableSMSLoginMethod && 'opacity-100',
)}
onClick={() => openSMSSettingsModal()}
>
Edit SMS settings
</button>
</div>
</div>
<div className="flex flex-row">
<Toggle
checked={enableSMSLoginMethod}
onChange={async () => {
if (enableSMSLoginMethod) {
await handleDisable();
} else {
openSMSSettingsModal();
}
}}
/>
</div>
</div>
<div className="mt-3 flex flex-row self-center align-middle">
<Text
variant="body"
size="normal"
color="greyscaleDark"
className="self-center"
>
Sign in users with Phone Number (SMS).
</Text>
</div>
</div>
</div>
</div>
</div>
);
}
export default EnableSMSSignIn;

View File

@@ -1,224 +0,0 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useSubmitState } from '@/hooks/useSubmitState';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import {
refetchGetAppInjectedVariablesQuery,
useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
export interface EditCustomUserJWTTokenData {
customUserJWTToken: string;
}
export type JWTSecretModalState = 'SHOW' | 'EDIT';
export interface JWTSecretModalProps {
close: () => void;
data?: any;
jwtSecret: string;
initialModalState?: JWTSecretModalState;
}
export function EditJWTSecretModal({ close }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { submitState, setSubmitState } = useSubmitState();
const [updateApplication] = useUpdateApplicationMutation({
refetchQueries: [
refetchGetAppInjectedVariablesQuery({ id: currentApplication.id }),
],
});
const {
handleSubmit,
control,
formState: { isSubmitting },
} = useForm<EditCustomUserJWTTokenData>({
reValidateMode: 'onSubmit',
defaultValues: {
customUserJWTToken: '',
},
});
const handleEditJWTSecret = async (data: EditCustomUserJWTTokenData) => {
setSubmitState({
error: null,
loading: false,
fieldsWithError: [],
});
try {
JSON.parse(data.customUserJWTToken);
} catch (error) {
setSubmitState({
error: new Error('The custom JWT token should be valid json.'),
loading: false,
fieldsWithError: [],
});
return;
}
try {
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
hasuraGraphqlJwtSecret: data.customUserJWTToken,
},
},
});
triggerToast(
`Successfully added custom JWT token to ${currentApplication.name}.`,
);
close();
} catch (error) {
triggerToast(
`Error adding custom JWT token to ${currentApplication.name}`,
);
setSubmitState({ error, loading: false, fieldsWithError: [] });
}
};
return (
<form
className="w-modal px-6 py-4"
onSubmit={handleSubmit(handleEditJWTSecret)}
>
<div className="grid grid-flow-row gap-2">
<div className="grid grid-flow-row text-left">
<Text variant="h3" component="h2">
Add Custom JWT Secret
</Text>
<Text variant="subtitle2">
You can add your custom JWT key here. Hasura will use this key to
validate the identity of your users.
</Text>
</div>
{submitState.error && (
<Alert severity="error">{submitState.error.message}</Alert>
)}
<Controller
name="customUserJWTToken"
control={control}
rules={{
required: true,
}}
render={({ field }) => (
<Input
{...field}
placeholder="Paste your custom JWT token here..."
componentsProps={{
inputRoot: {
className: 'font-mono bg-header',
},
}}
aria-label="Custom JWT token"
type="text"
value={field.value}
onBlur={() =>
setSubmitState({
error: null,
loading: false,
fieldsWithError: [],
})
}
multiline
rows={6}
fullWidth
hideEmptyHelperText
/>
)}
/>
<div className="grid grid-flow-row gap-2">
<Button type="submit" loading={isSubmitting}>
Save Changes
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</form>
);
}
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
return (
<div className="w-modal px-6 py-4">
<div className="grid grid-flow-row gap-2">
<div className="grid grid-flow-row text-left">
<Text variant="h3" component="h2">
Auth JWT Secret
</Text>
<Text variant="subtitle2">
This is the key used for generating JWTs. It&apos;s HMAC-SHA-based
and the same as configured in Hasura.
</Text>
</div>
<div>
<Input
defaultValue={JWTKey}
multiline
disabled
fullWidth
hideEmptyHelperText
rows={6}
componentsProps={{
inputRoot: { className: 'font-mono' },
}}
/>
</div>
<div className="mx-auto max-w-sm text-center">
<Text variant="subtitle2">
Already using a third party auth service? <br />
<button
type="button"
className="mt-0.5 ml-0.5 text-xs font-medium text-blue"
onClick={() => {
editJWTSecret();
}}
>
Add your custom JWT token
</button>
</Text>
</div>
</div>
</div>
);
}
export function JWTSecretModal({
close,
data,
jwtSecret,
initialModalState,
}: any) {
const [jwtSecretModalState, setJwtSecretModalState] =
useState<JWTSecretModalState>(initialModalState || 'SHOW');
const editJWTSecret = () => {
setJwtSecretModalState('EDIT');
};
if (jwtSecretModalState === 'EDIT') {
return <EditJWTSecretModal close={close} />;
}
return (
<ShowJWTTokenModal
JWTKey={jwtSecret || data}
editJWTSecret={editJWTSecret}
/>
);
}

View File

@@ -1,28 +0,0 @@
import { Avatar } from '@/ui/Avatar';
import { Text } from '@/ui/Text';
import { nhost } from '@/utils/nhost';
import Image from 'next/image';
export function SelectedWorkspaceOnNewApp({ current }: any) {
const user = nhost.auth.getUser();
return (
<div className="flex flex-row space-x-2 self-center">
{current.name === 'Default Workspace' ? (
<Avatar
className="h-5 w-5 self-center rounded-full"
name={user?.displayName}
avatarUrl={user?.avatarUrl}
/>
) : (
<div className="h-5 w-5 overflow-hidden rounded-md">
<Image src="/logos/new.svg" alt="Nhost Logo" width={20} height={20} />
</div>
)}
<Text size="small" color="greyscaleDark" className="font-normal">
{current.name}
</Text>
</div>
);
}
export default SelectedWorkspaceOnNewApp;

View File

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

View File

@@ -1,17 +0,0 @@
import { Text } from '@/ui/Text';
interface ErrorComponentProps {
message: string;
}
function ErrorComponent({ message }: ErrorComponentProps) {
return (
<div className="my-4 rounded-md bg-warning px-4 py-2 text-dark">
<Text className="font-medium text-textOrange">Error</Text>
<Text className="pt-2 font-medium text-dimBlack" size="normal">
{message}
</Text>
</div>
);
}
export default ErrorComponent;

View File

@@ -1,22 +0,0 @@
function Copy({ stroke = '#21324B', ...props }: any) {
return (
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M10.5 10.5h3v-8h-8v3"
stroke={stroke}
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.5 5.5h-8v8h8v-8z"
stroke={stroke}
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default Copy;

View File

@@ -1,29 +0,0 @@
import * as React from 'react';
function Eye(props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
);
}
export default Eye;

View File

@@ -1,25 +0,0 @@
import * as React from 'react';
function EyeOff(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
);
}
export default EyeOff;

View File

@@ -1,14 +0,0 @@
export default function Lock(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
) {
return (
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 1.75a1.5 1.5 0 0 0-1.5 1.5v1.5h3v-1.5A1.5 1.5 0 0 0 8 1.75Zm-3 1.5v1.5H3c-.69 0-1.25.56-1.25 1.25v7c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25V6c0-.69-.56-1.25-1.25-1.25h-2v-1.5a3 3 0 0 0-6 0Zm-1.75 3h9.5v6.5h-9.5v-6.5ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
fill="#21324B"
/>
</svg>
);
}

View File

@@ -1,21 +0,0 @@
import * as React from 'react';
function Plus(props: any) {
return (
<svg
viewBox="0 0 32 32"
stroke="currentColor"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 4.75C9.787 4.75 4.75 9.787 4.75 16S9.787 27.25 16 27.25 27.25 22.213 27.25 16 22.213 4.75 16 4.75zM3.25 16C3.25 8.958 8.958 3.25 16 3.25S28.75 8.958 28.75 16 23.042 28.75 16 28.75 3.25 23.042 3.25 16zm12 .75H10v-1.5h5.25V10h1.5v5.25H22v1.5h-5.25V22h-1.5v-5.25z"
/>
</svg>
);
}
export default Plus;

View File

@@ -1,20 +0,0 @@
import * as React from 'react';
function TwilioIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={108}
height={32}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M16.864 0c8.844 0 15.984 7.147 15.984 16s-7.14 16-15.984 16C8.019 32 .88 24.853.88 16S8.02 0 16.864 0Zm0 4.267c-6.5 0-11.722 5.226-11.722 11.733a11.694 11.694 0 0 0 11.722 11.733c6.5 0 11.721-5.226 11.721-11.733A11.694 11.694 0 0 0 16.864 4.267Zm27.173 2.026c.213-.106.426.107.426.214v4.586h8.525c.106 0 .32.214.32.32l.639 2.667.639 2.667.107.32.106-.32 1.599-5.334c0-.213.213-.32.32-.32h4.262c.106 0 .32.214.32.32l1.704 5.654.107-.32 1.385-5.334c0-.213.213-.32.32-.32h10.869c.106 0 .213.214.213.32V25.6c0 .213-.213.32-.32.32h-5.434c-.213 0-.32-.213-.32-.32V12.16l-4.05 13.44c0 .187-.162.292-.275.315l-.044.005H60.98c-.107 0-.32-.213-.32-.32l-.853-2.88-.959-3.093L56.93 25.6c0 .213-.213.32-.32.32h-4.475c-.106 0-.32-.213-.32-.32l-4.049-13.44v3.413c0 .214-.213.32-.32.32h-3.09v3.627c0 1.067.533 1.493 1.492 1.493.426 0 .96-.106 1.492-.32.106-.106.32 0 .32.214v4.266c-.96.534-2.345.854-3.836.854-3.517 0-5.435-1.6-5.435-5.12v-5.014h-1.385c-.213 0-.32-.213-.32-.32V11.52c0-.213.213-.32.32-.32h1.385V8.32c0-.213.106-.32.32-.32l5.328-1.707Zm54.984 4.48c4.689 0 8.099 3.52 8.099 7.574 0 4.053-3.41 7.573-8.205 7.573-4.689 0-8.099-3.52-8.099-7.573 0-4.054 3.41-7.574 8.205-7.574Zm-16.197-4.48c.213 0 .32.107.32.214v18.986c0 .214-.213.32-.32.32H77.39c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.434Zm7.14 4.8c.213 0 .32.214.32.32v13.974c0 .213-.214.32-.32.32h-5.435c-.213 0-.32-.214-.32-.32V11.413c0-.213.214-.32.32-.32h5.435ZM12.92 16.64a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.303-3.306 3.322 3.322 0 0 1 3.303-3.307Zm7.886 0a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.304-3.306 3.322 3.322 0 0 1 3.304-3.307Zm78.214-.747c-1.385 0-2.344 1.174-2.344 2.56 0 1.387 1.066 2.56 2.344 2.56 1.386 0 2.345-1.173 2.345-2.56 0-1.493-1.066-2.666-2.345-2.56ZM20.807 8.747a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.304-3.307 3.322 3.322 0 0 1 3.304-3.306Zm-7.886 0a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.303-3.307 3.322 3.322 0 0 1 3.303-3.306Zm62.87-2.454c.107 0 .213.107.32.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Zm14.28 0c.213 0 .319.107.319.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Z"
fill="#F22F46"
/>
</svg>
);
}
export default TwilioIcon;

View File

@@ -11,7 +11,7 @@ import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword/generateRandomDatabasePassword';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword';
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';

View File

@@ -1,30 +0,0 @@
import clsx from 'clsx';
import type { HTMLAttributes } from 'react';
export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
/**
* Determines the vertical margin of the divider.
*
* @default 'high'
*/
spacing?: 'low' | 'medium' | 'high';
/**
* Arbitrary classnames to be added to the divider.
*
*/
className?: string;
}
export function Divider({ spacing = 'high', className }: DividerProps) {
return (
<div
className={clsx(
'order-3 mx-auto h-[0.25px] w-full self-stretch bg-verydark opacity-20',
spacing === 'low' && 'my-12',
spacing === 'medium' && 'my-16',
spacing === 'high' && 'my-20',
className,
)}
/>
);
}

View File

@@ -1,121 +0,0 @@
import clsx from 'clsx';
import type {
DetailedHTMLProps,
ForwardedRef,
HTMLProps,
ReactNode,
} from 'react';
import { forwardRef } from 'react';
export interface InputFieldProps
extends DetailedHTMLProps<HTMLProps<HTMLInputElement>, HTMLInputElement> {
/**
* Props to be passed to the input wrapper.
*/
wrapperProps?: Omit<
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>,
'children'
>;
/**
* Props to be passed to the label element.
*/
labelProps?: Omit<
DetailedHTMLProps<HTMLProps<HTMLLabelElement>, HTMLLabelElement>,
'htmlFor' | 'children'
>;
/**
* Input label.
*/
label?: string;
/**
* Start input adornment for this component.
*/
startAdornment?: ReactNode;
/**
* Error to be displayed.
*/
error?: ReactNode;
}
const InlineInput = forwardRef(
(
{
label,
labelProps,
startAdornment,
wrapperProps,
className,
error,
...props
}: InputFieldProps,
ref: ForwardedRef<HTMLInputElement>,
) => {
const { className: labelClassName, ...restLabelProps } = labelProps || {};
const { className: wrapperClassName, ...restWrapperProps } =
wrapperProps || {};
return (
<div className="grid grid-flow-row gap-1">
<div
className={clsx(
'grid grid-cols-5 items-center gap-y-1 py-1.5',
wrapperClassName,
)}
{...restWrapperProps}
>
{label && (
<label
htmlFor={props.id}
className={clsx(
'col-span-2 text-sm+ font-medium text-greyscaleDark',
labelClassName,
)}
{...restLabelProps}
>
{label}
</label>
)}
<div
className={clsx(
'flex flex-row place-content-start items-center rounded-sm px-2 py-1',
error
? 'outline outline-2 outline-red'
: 'focus-within:outline focus-within:outline-2 focus-within:outline-blue',
label ? 'col-span-3' : 'col-span-5',
)}
>
{startAdornment && (
<label className="flex-shrink-0" htmlFor={props.id}>
{startAdornment}
</label>
)}
<input
className={clsx(
'h-full w-full rounded-sm+ px-0.5 font-display text-sm+ text-greyscaleDark focus:outline-none',
className,
)}
aria-invalid={Boolean(error)}
ref={ref}
{...props}
/>
</div>
{error && (
<div
className="col-span-5 text-right text-xs text-red"
role="alert"
>
{error}
</div>
)}
</div>
</div>
);
},
);
InlineInput.displayName = 'NhostInlineInput';
export default InlineInput;

View File

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

View File

@@ -1,77 +0,0 @@
import type { PrefetchNewAppPlansFragment } from '@/generated/graphql';
import { Text } from '@/ui/Text';
import Checkbox from '@/ui/v2/Checkbox';
import { planDescriptions } from '@/utils/planDescriptions';
import { RadioGroup } from '@headlessui/react';
import clsx from 'clsx';
import React from 'react';
interface PlanSelectorProps {
options: PrefetchNewAppPlansFragment[];
value: PrefetchNewAppPlansFragment;
onChange:
| React.Dispatch<React.SetStateAction<PrefetchNewAppPlansFragment>>
| any;
}
export function PlanSelector({ options, value, onChange }: PlanSelectorProps) {
return (
<RadioGroup value={value} onChange={onChange}>
<RadioGroup.Label className="sr-only">Pricing plans</RadioGroup.Label>
<div className="relative divide-y-1 border-t-1 border-b-1 bg-white">
{options.map((plan) => (
<RadioGroup.Option key={plan.name} value={plan}>
{({ checked }) => (
<div className="cu flex cursor-pointer flex-row place-content-between items-center py-4 font-display">
<RadioGroup.Label
as="div"
className="flex flex-row font-medium"
>
<Checkbox
aria-describedby="plan"
checked={plan.name === value.name}
/>
<div className="flex w-80">
<div className=" self-center pl-2 text-xs font-medium text-greyscaleDark">
<span className="font-bold">{plan.name}:</span>{' '}
<span className="leading-4">
{planDescriptions[plan.name]}
</span>
</div>
</div>
</RadioGroup.Label>
<div className="flex">
<span
className={clsx(
'self-center font-medium',
checked ? 'text-indigo-900' : 'text-black',
)}
>
<div className="mr-3 self-center text-lg text-greyscaleDark">
{plan.isFree ? (
'Free'
) : (
<div className="flex flex-row">
$ {plan.price}{' '}
<Text
size="tiny"
className="ml-1 self-center tracking-wide"
>
/ mo
</Text>
</div>
)}
</div>
</span>
</div>
</div>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
);
}
export default PlanSelector;

View File

@@ -1,49 +0,0 @@
import type { ForwardedRef, PropsWithChildren } from 'react';
import { forwardRef } from 'react';
export interface TooltipProps extends PropsWithChildren<unknown> {
/**
* Title of the tooltip.
*/
title: string;
/**
* Determine if the tooltip should be shown.
*/
disabled?: boolean;
}
export const Tooltip = forwardRef(
(
{ title, children, disabled }: TooltipProps,
ref: ForwardedRef<HTMLDivElement>,
) => (
<div className="group relative" ref={ref}>
{children}
{!disabled && (
<div className="absolute left-2 bottom-1 z-50 hidden group-hover:block">
<svg
className="absolute -top-2 left-3 z-50 mr-3 h-3 w-3 -rotate-180 transform text-greyscaleDark"
x="0px"
y="0px"
viewBox="0 0 255 255"
xmlSpace="preserve"
>
<polygon
className="border border-greyscaleDark fill-current text-greyscaleDark"
points="0,0 127.5,127.5 255,0"
/>
</svg>
<div className="text-sharp absolute left-0 z-50 mt-1 w-40 origin-top-left rounded-md bg-greyscaleDark p-2 font-display text-sm- font-medium text-white shadow-2xl">
{title}
</div>
</div>
)}
</div>
),
);
Tooltip.displayName = 'NhostTooltip';
export default Tooltip;

View File

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

View File

@@ -1,29 +0,0 @@
import { Avatar } from '@/ui/Avatar';
import { Text } from '@/ui/Text';
import { nhost } from '@/utils/nhost';
import Image from 'next/image';
export function WorkspaceSelectorNewApp({ option }: any) {
const user = nhost.auth.getUser();
return (
<div className="flex flex-row items-center py-0.5">
{option.name === 'Default Workspace' ? (
<Avatar
className="h-6 w-6 rounded-full"
name={user?.displayName}
avatarUrl={user?.avatarUrl}
/>
) : (
<div className="h-6 w-6 overflow-hidden rounded-md">
<Image src="/logos/new.svg" alt="Nhost Logo" width={24} height={24} />
</div>
)}
<Text className="ml-2 font-medium" color="greyscaleDark">
{option.name}
</Text>
</div>
);
}
export default WorkspaceSelectorNewApp;

View File

@@ -1,15 +0,0 @@
import { useRouter } from 'next/router';
export const useGetAppURL = (): {
workspaceSlug: string;
appSlug: string;
} => {
const router = useRouter();
const workspaceSlug = router.query.workspaceSlug as string;
const appSlug = router.query.appSlug as string;
return { workspaceSlug, appSlug };
};
export default useGetAppURL;

View File

@@ -1,5 +1,5 @@
import Container from '@/components/layout/Container';
import AllowedEmailDomainsSettings from '@/components/settings/authentication/AllowedEmailSettings/AllowedEmailSettings';
import AllowedEmailDomainsSettings from '@/components/settings/authentication/AllowedEmailSettings';
import AllowedRedirectURLsSettings from '@/components/settings/authentication/AllowedRedirectURLsSettings';
import BlockedEmailSettings from '@/components/settings/authentication/BlockedEmailSettings';
import ClientURLSettings from '@/components/settings/authentication/ClientURLSettings';

View File

@@ -1,6 +1,6 @@
import Container from '@/components/layout/Container';
import PermissionVariableSettings from '@/components/settings/permissions/PermissionVariableSettings';
import RolesSettings from '@/components/settings/roles/RoleSettings/RoleSettings';
import RolesSettings from '@/components/settings/roles/RoleSettings';
import SettingsLayout from '@/components/settings/SettingsLayout';
import type { ReactElement } from 'react';

View File

@@ -23,7 +23,7 @@ import { getErrorMessage } from '@/utils/getErrorMessage';
import { getCurrentEnvironment, slugifyString } from '@/utils/helpers';
import { nhost } from '@/utils/nhost';
import { planDescriptions } from '@/utils/planDescriptions';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword/generateRandomDatabasePassword';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword';
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
import { triggerToast } from '@/utils/toast';

View File

@@ -1,10 +0,0 @@
export type Provider = {
name: string;
logo: string;
active: boolean;
docsLink: string;
};
export type Providers = {
providers: Provider[];
};

View File

@@ -1,49 +0,0 @@
import { resolveProvider } from './resolveProvider';
export function getDynamicVariables(providerId, vars, prefill = false) {
const authEnabled = `auth${resolveProvider(providerId as string)}Enabled`;
const authClientId = `auth${resolveProvider(providerId as string)}ClientId`;
const authClientSecret = `auth${resolveProvider(
providerId as string,
)}ClientSecret`;
// @TODO: check prefill, use only one function: there's another one with the same functionality
// in providerId.tsx.
if (providerId === 'twitter') {
return {
authEnabled: 'authTwitterEnabled',
authClientId: 'authTwitterConsumerKey',
authClientSecret: 'authTwitterConsumerSecret',
};
}
if (providerId === 'apple') {
return {
authEnabled: 'authAppleEnabled',
authClientId: 'authAppleKeyId',
authClientSecret: 'authApplePrivateKey',
};
}
const {
authProviderEnabled,
authProviderClientId,
authProviderClientSecret,
} = vars;
if (prefill) {
return {
authEnabled,
authClientId,
authClientSecret,
};
}
return {
[authEnabled]: authProviderEnabled,
[authClientId]: authProviderClientId,
[authClientSecret]: authProviderClientSecret,
};
}
export default getDynamicVariables;

View File

@@ -1,15 +0,0 @@
import { capitalize } from './helpers';
export const resolveProvider = (providerId: string) => {
if (providerId.toLowerCase() === 'microsoft') {
return 'WindowsLive';
}
if (providerId.toLowerCase() === 'workos') {
return 'WorkOs';
}
return capitalize(providerId);
};
export default resolveProvider;

View File

@@ -1,23 +0,0 @@
import validator from 'validator';
export const validateDomainsInput = (domainsFromInput: string) => {
if (domainsFromInput.length === 0) {
return false;
}
const domains: string[] = [];
let checkedDomains: boolean[] = [];
domainsFromInput.split(',').forEach((email) => {
domains.push(email.replace(' ', ''));
});
checkedDomains = domains.map((domain) => {
if (!validator.isFQDN(domain)) {
return false;
}
return true;
});
return checkedDomains.includes(false);
};
export default validateDomainsInput;

View File

@@ -1,23 +0,0 @@
import validator from 'validator';
export const validateEmailInputs = (emailsFromInput: string) => {
if (emailsFromInput.length === 0) {
return false;
}
const emails: string[] = [];
let checkEmails: boolean[] = [];
emailsFromInput.split(',').forEach((email) => {
emails.push(email.replace(' ', ''));
});
checkEmails = emails.map((email) => {
if (!validator.isEmail(email)) {
return false;
}
return true;
});
return checkEmails.includes(false);
};
export default validateEmailInputs;