Compare commits

...

59 Commits

Author SHA1 Message Date
Pilou
4a6e8c9945 Merge pull request #1263 from nhost/renovate/akhilmhdh-contributors-readme-action-2.x
chore(deps): update akhilmhdh/contributors-readme-action action to v2.3.6
2022-12-01 15:06:20 +01:00
Pierre-Louis Mercereau
3a4d087e6f chore: remove changeset 2022-12-01 15:04:52 +01:00
Szilárd Dóró
ee7338cc12 Merge pull request #1266 from nhost/changeset-release/main
chore: update versions
2022-12-01 14:10:20 +01:00
github-actions[bot]
ea0b35fe84 chore: update versions 2022-12-01 12:30:09 +00:00
Szilárd Dóró
4fbd22f232 Merge pull request #1258 from nhost/fix/settings-env-var-character-limit
fix(dashboard): remove character limit from env var inputs
2022-12-01 13:28:30 +01:00
renovate[bot]
a7ab1243a4 chore(deps): update akhilmhdh/contributors-readme-action action to v2.3.6 2022-12-01 10:43:54 +00:00
Pilou
5638b5e770 Merge pull request #1262 from nhost/ci/adjust-workflows
ci: adjust workflows
2022-12-01 11:43:09 +01:00
renovate[bot]
c5ce895c97 chore(deps): update akhilmhdh/contributors-readme-action action to v2.3.6 2022-12-01 10:42:20 +00:00
Pierre-Louis Mercereau
2be99c0e55 ci: adjust workflows 2022-12-01 11:34:59 +01:00
Pilou
f6299b537d Merge pull request #1261 from plmercereau/main
ci: set Renovate bot
2022-12-01 11:33:30 +01:00
Pilou
11d06cce4b Merge branch 'nhost:main' into main 2022-12-01 10:49:46 +01:00
Pierre-Louis Mercereau
e9e0ad6d3c ci: clean renovate 2022-12-01 10:46:28 +01:00
Pierre-Louis Mercereau
3ad2d5ced8 ci: clean-up 2022-12-01 10:44:47 +01:00
Szilárd Dóró
42bfa7c16d Merge pull request #1259 from nhost/chore/issue-templates
chore: create issue templates
2022-12-01 10:37:51 +01:00
Pierre-Louis Mercereau
0dea974a2e ci: fine-tune 2022-12-01 10:28:13 +01:00
Szilárd Dóró
8165b8f13e chore: create issue templates 2022-12-01 10:25:30 +01:00
Pierre-Louis Mercereau
1c8864c992 ci: debug ref_head 2022-12-01 10:22:40 +01:00
Pierre-Louis Mercereau
dd6921676f ci: correct sed 2022-12-01 10:12:54 +01:00
Szilárd Dóró
a1193da4fb fix(dashboard): remove character limit from env var inputs 2022-12-01 10:05:56 +01:00
Pierre-Louis Mercereau
ff83a29dcc ci: fix filter 2022-12-01 10:03:15 +01:00
Pierre-Louis Mercereau
4771bff65c ci: don't use ambiguous redirection 2022-12-01 09:56:57 +01:00
Pierre-Louis Mercereau
6106265d87 ci: install pnpm 2022-12-01 09:48:48 +01:00
Pierre-Louis Mercereau
3c2455b7e1 ci: echo 2022-12-01 09:39:59 +01:00
Pierre-Louis Mercereau
7bc8de103b ci: correct 2022-12-01 09:36:26 +01:00
Pierre-Louis Mercereau
e49ba4bd02 ci: don't use paste 2022-12-01 09:23:24 +01:00
Pierre-Louis Mercereau
44ac1a0095 ci: shuf 2022-12-01 09:10:41 +01:00
Pierre-Louis Mercereau
04ce686e7c Merge branch 'main' of https://github.com/plmercereau/nhost 2022-12-01 08:46:29 +01:00
Pierre-Louis Mercereau
ca01224083 ci: alway patch 2022-12-01 08:46:02 +01:00
Pilou
d489232cbc Update packages.yaml 2022-12-01 07:43:32 +01:00
Pierre-Louis Mercereau
9efc0522c3 fix(examples): rename test script to e2e:test 2022-11-30 23:54:57 +01:00
Pierre-Louis Mercereau
4c86ed787e ci: shorter job title 2022-11-30 23:50:28 +01:00
Pierre-Louis Mercereau
9a3c05f69b fix(examples): correct examples to pass the build step 2022-11-30 23:48:11 +01:00
Pierre-Louis Mercereau
2b03f80d59 ci: avoid double negative 2022-11-30 23:37:40 +01:00
Pierre-Louis Mercereau
baa3f0531d ci: typo 2022-11-30 23:35:49 +01:00
Pierre-Louis Mercereau
f9bbc78241 ci: correct test 2022-11-30 23:34:37 +01:00
Pierre-Louis Mercereau
7f62c8b7c2 refactor(examples): remove obsolete test 2022-11-30 23:31:04 +01:00
Pierre-Louis Mercereau
de2bcb6320 ci: fix 2022-11-30 23:18:42 +01:00
Pierre-Louis Mercereau
64a6d934e7 ci: dict 2022-11-30 23:17:26 +01:00
Pierre-Louis Mercereau
f455022e6d ci: debug 2022-11-30 23:15:26 +01:00
Pierre-Louis Mercereau
baac8a3605 ci: debug 2022-11-30 23:03:39 +01:00
Pierre-Louis Mercereau
be20cd5cdb ci: remove if 2022-11-30 22:58:17 +01:00
Pilou
1de4137369 Merge pull request #4 from plmercereau/renovate/configure
Configure Renovate
2022-11-30 22:54:17 +01:00
Pierre-Louis Mercereau
18fae1a891 ci(renovate): ignore node and pnpm 2022-11-30 22:54:04 +01:00
Pierre-Louis Mercereau
03c58613b8 ci: adapt renovate 2022-11-30 22:49:46 +01:00
Pierre-Louis Mercereau
fb23dd6b39 ci: prepare renovate 2022-11-30 22:45:33 +01:00
renovate[bot]
c12bd632fc chore(deps): add renovate.json 2022-11-30 20:11:34 +00:00
Pilou
a71eea09a9 Merge pull request #1254 from nhost/ci/create-cli-pr
ci: PR to the CLI with the new dashboard version
2022-11-30 18:25:47 +01:00
Pierre-Louis Mercereau
d1f7842169 chore: set fetch-depth to 0 2022-11-30 16:50:32 +01:00
Pierre-Louis Mercereau
4566c2a202 chore: rephrase 2022-11-30 16:40:31 +01:00
Pierre-Louis Mercereau
c678918e73 ci: PR to the CLI with the new dashboard version 2022-11-30 16:38:03 +01:00
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ó
75c7ba7f12 Merge pull request #1252 from nhost/changeset-release/main
chore: update versions
2022-11-30 08:29:25 +01:00
github-actions[bot]
d62dfe19a2 chore: update versions 2022-11-30 07:28:00 +00:00
Szilárd Dóró
0dbef188b1 Merge pull request #1247 from nhost/chore/sidebar-menu-item-height
chore(dashboard): update settings sidebar menu item density
2022-11-30 08:26:39 +01:00
Szilárd Dóró
44f13f6240 chore(dashboard): cleanup unused files 2022-11-29 20:29:25 +01:00
Szilárd Dóró
e01cb2ed49 chore(dashboard): add changeset 2022-11-29 17:18:48 +01:00
Szilárd Dóró
388eef041f chore(dashboard): change settings sidebar menu item density 2022-11-29 17:18:09 +01:00
50 changed files with 196 additions and 1487 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -5,6 +5,10 @@ inputs:
description: 'Turborepo token'
TURBO_TEAM:
description: 'Turborepo team'
BUILD:
description: 'Build packages'
default: 'default'
runs:
using: 'composite'
steps:
@@ -35,6 +39,14 @@ runs:
# * They are reused through the Turborepo cache
- shell: bash
name: Build packages
if: ${{ inputs.BUILD == 'all' }}
run: pnpm build:all
env:
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
- shell: bash
name: Build everything in the monorepo
if: ${{ inputs.BUILD == 'default' }}
run: pnpm build
env:
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}

10
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"ignoreDeps": [
"pnpm",
"node"
]
}

View File

@@ -148,3 +148,33 @@ jobs:
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
bump-cli:
name: Bump Dashboard version in the Nhost CLI
runs-on: ubuntu-latest
needs:
- version
- publish-docker
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
repository: nhost/cli
token: ${{ secrets.GH_PAT }}
fetch-depth: 0
- name: Bump version in source code
run: |
IMAGE=$(echo ${{ env.DASHBOARD_PACKAGE }} | sed 's/@\(.\+\)\/\(.\+\)/\1\\\/\2/g')
VERSION="${{ needs.version.outputs.dashboardVersion }}"
EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g'
find ./ -type f -exec sed -i -e $EXPRESSION {} \;
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GH_PAT }}
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
branch: bump-dashboard-version
delete-branch: true
title: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
body: |
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.

View File

@@ -1,11 +1,9 @@
name: Packages
name: Continuous Integration
on:
push:
branches: [main]
paths-ignore:
- 'dashboard/**'
- 'docs/**'
- 'assets/**'
- '**.md'
- 'LICENSE'
@@ -13,16 +11,55 @@ on:
branches: [main]
types: [opened, synchronize]
paths-ignore:
- 'dashboard/**'
- 'docs/**'
- 'assets/**'
- '**.md'
- 'LICENSE'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
jobs:
renovate-changeset:
name: Add Renovate Changesets
if: ${{ github.event_name == 'pull_request' && github.event.action == 'opened' && startsWith(github.head_ref, 'renovate/') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
BUILD: 'none'
- name: Install dictionary
run: sudo apt-get install wbritish
- name: Generate changeset file name
id: file_name
run: |
FILE_NAME=$(shuf -n 3 /usr/share/dict/words | tr '\n' '-' | sed 's/-$//' | sed 's/'"'"'s//g' | tr '[:upper:]' '[:lower:]')
echo "result=./.changeset/${FILE_NAME}.md" >> $GITHUB_OUTPUT
- name: Create changeset file
run: |
echo "---" > ${{ steps.file_name.outputs.result }}
pnpm recursive list --depth -1 --parseable \
--filter='!nhost-root' \
--filter=...[origin/${{ github.base_ref }}] \
| xargs -I@ jq ".name" @/package.json \
| sort \
| uniq -u \
| awk '$0=$0": patch"' \
>> ${{ steps.file_name.outputs.result }}
echo "---" >> ${{ steps.file_name.outputs.result }}
echo >> ${{ steps.file_name.outputs.result }}
echo "${{ github.event.pull_request.title }}" >> ${{ steps.file_name.outputs.result }}
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: ${{ github.event.pull_request.title }}
build:
name: Build @nhost packages
runs-on: ubuntu-latest
@@ -36,6 +73,7 @@ jobs:
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
BUILD: 'all'
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
# * In a PR, only include packages that have been modified, and their dependencies
- name: List examples with an e2e script
@@ -103,7 +141,7 @@ jobs:
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
- name: Run unit tests
run: pnpm run test
run: pnpm run test:all
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
@@ -127,4 +165,4 @@ jobs:
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
- name: Lint
run: pnpm run lint
run: pnpm run lint:all

View File

@@ -10,6 +10,6 @@ jobs:
name: A job to automate contrib in readme
steps:
- name: Contribute List
uses: akhilmhdh/contributors-readme-action@v2.3.4
uses: akhilmhdh/contributors-readme-action@v2.3.6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,24 +2,17 @@ name: 'Dashboard'
on:
workflow_call:
pull_request:
branches: [main]
types: [opened, synchronize]
paths:
- 'packages/**'
- 'dashboard/**'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
steps:
- uses: actions/checkout@v3
- name: Install Node and dependencies
@@ -33,10 +26,6 @@ jobs:
tests:
name: Tests
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
steps:
- uses: actions/checkout@v3
- name: Install Node and dependencies
@@ -50,10 +39,6 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
steps:
- uses: actions/checkout@v3
- name: Install Node and dependencies

View File

@@ -1,5 +1,23 @@
# @nhost/dashboard
## 0.7.3
### Patch Changes
- a1193da4: fix(dashboard): remove character limit from env var inputs
## 0.7.2
### Patch Changes
- 44f13f62: chore(dashboard): cleanup unused files
## 0.7.1
### Patch Changes
- e01cb2ed: chore(dashboard): change settings sidebar menu item density
## 0.7.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.7.0",
"version": "0.7.3",
"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

@@ -48,6 +48,7 @@ function SettingsNavLink({
return (
<ListItem.Root>
<ListItem.Button
dense
href={finalUrl}
component={NavLink}
selected={active}

View File

@@ -130,7 +130,6 @@ export default function BaseEnvironmentVariableForm({
}
},
})}
inputProps={{ maxLength: 100 }}
id="name"
label="Name"
placeholder="EXAMPLE_NAME"
@@ -145,7 +144,6 @@ export default function BaseEnvironmentVariableForm({
<Input
{...register('prodValue')}
inputProps={{ maxLength: 100 }}
id="prodValue"
label="Production Value"
placeholder="Enter value"
@@ -159,7 +157,6 @@ export default function BaseEnvironmentVariableForm({
<Input
{...register('devValue')}
inputProps={{ maxLength: 100 }}
id="devValue"
label="Development Value"
placeholder="Enter value"

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

@@ -34,7 +34,7 @@ const StyledListItemButton = styled(MaterialListItemButton)(({ theme }) => ({
alignItems: 'center',
gap: theme.spacing(),
[`&.${getListItemButtonUtilityClass('dense')}`]: {
padding: theme.spacing(0.75, 1.25),
padding: theme.spacing(1, 1.25),
},
[`&.${listItemButtonClasses.selected}`]: {
backgroundColor: theme.palette.primary.light,

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;

View File

@@ -23,8 +23,8 @@
"dev": "vite --host localhost --port 3000",
"generate": "graphql-codegen --config graphql.config.yaml",
"cypress": "cypress open",
"test": "cypress run",
"e2e": "start-test e2e:backend http-get://localhost:9695 e2e:frontend 3000 test",
"e2e": "start-test e2e:backend http-get://localhost:9695 e2e:frontend 3000 e2e:test",
"e2e:test": "cypress run",
"e2e:backend": "nhost dev --no-browser",
"e2e:frontend": "run-s build preview",
"build": "vite build",
@@ -67,4 +67,4 @@
"ws": "^8.8.1",
"xstate": "^4.33.5"
}
}
}

View File

@@ -0,0 +1,5 @@
declare module 'react/jsx-runtime' {
const content: string;
export default content;
}

View File

@@ -1,13 +1,6 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {
const $: typeof import('vue/macros')['$']
const $$: typeof import('vue/macros')['$$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computedAsync: typeof import('@vueuse/core')['computedAsync']

View File

@@ -1,3 +0,0 @@
// Vitest Snapshot v1
exports[`Counter.vue > should render 1`] = `"<div>10 <button class=\\"inc\\"> + </button><button class=\\"dec\\"> - </button></div>"`;

View File

@@ -1,22 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import Counter from '../src/components/Counter.vue'
describe('Counter.vue', () => {
it('should render', () => {
const wrapper = mount(Counter, { props: { initial: 10 } })
expect(wrapper.text()).toContain('10')
expect(wrapper.html()).toMatchSnapshot()
})
it('should be interactive', async () => {
const wrapper = mount(Counter, { props: { initial: 0 } })
expect(wrapper.text()).toContain('0')
expect(wrapper.find('.inc').exists()).toBe(true)
await wrapper.get('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
})

View File

@@ -16,7 +16,7 @@ export default defineConfig({
},
plugins: [
Vue({
reactivityTransform: true
reactivityTransform: path.resolve(__dirname, 'src'),
}),
// https://github.com/hannoeru/vite-plugin-pages
@@ -24,7 +24,7 @@ export default defineConfig({
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: ['vue/macros', 'vue-router', '@vueuse/core'],
imports: ['vue-router', '@vueuse/core'],
dts: true
}),

View File

@@ -35,6 +35,7 @@
"release": "pnpm run prerelease && changeset publish",
"snapshot": "pnpm prerelease && changeset version --snapshot preview && pnpm install && changeset publish --tag preview",
"test": "turbo run test --filter=!@nhost/dashboard --filter=!@nhost/docs --filter=!@nhost-examples/* --no-deps --include-dependencies",
"test:all": "turbo run test",
"test:dashboard": "turbo run test --filter=@nhost/dashboard",
"e2e": "turbo run e2e --concurrency=1",
"changeset": "changeset",