Compare commits
36 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9b84c7658 | ||
|
|
c78a765941 | ||
|
|
72899a600f | ||
|
|
fe6e8e2d15 | ||
|
|
737945bd0b | ||
|
|
8f77914eb3 | ||
|
|
839ca68f74 | ||
|
|
10b0f7490e | ||
|
|
9cb18747e8 | ||
|
|
d872d45a60 | ||
|
|
7324d8c089 | ||
|
|
7a50849ab3 | ||
|
|
b0558fcb19 | ||
|
|
5f94486faf | ||
|
|
2e58b9fd26 | ||
|
|
eb9539277b | ||
|
|
65c01c1e81 | ||
|
|
8e4282b094 | ||
|
|
81e1d78315 | ||
|
|
0d43bd2c3b | ||
|
|
6f324afcae | ||
|
|
c1eff3a66b | ||
|
|
c774efee40 | ||
|
|
b36aa6041d | ||
|
|
9ed9857e17 | ||
|
|
b4b057e12c | ||
|
|
5b087257e4 | ||
|
|
f1b2117c37 | ||
|
|
c1514eb098 | ||
|
|
21bddeed6a | ||
|
|
84dd864186 | ||
|
|
e2c7741468 | ||
|
|
89fd97cbf0 | ||
|
|
f830a9d5f2 | ||
|
|
9a7e431323 | ||
|
|
1f2b0dced4 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -22,6 +22,10 @@ env:
|
|||||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||||
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
||||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||||
|
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
|
||||||
|
NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }}
|
||||||
|
NHOST_TEST_PERSONAL_ORG_SLUG: ${{ vars.NHOST_TEST_PERSONAL_ORG_SLUG }}
|
||||||
|
NHOST_TEST_PROJECT_SUBDOMAIN: ${{ vars.NHOST_TEST_PROJECT_SUBDOMAIN }}
|
||||||
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
|
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
|
||||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: "gen: update depenendencies"
|
name: "gen: update depenendencies"
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 2 1 * *'
|
- cron: '0 2 1 2,5,8,11 *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run:
|
run:
|
||||||
|
|||||||
17
changelog_summary.sh
Executable file
17
changelog_summary.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#/usr/bin/env bash
|
||||||
|
PREV_MONTH=$(date -d "1 month ago" +%Y-%m)
|
||||||
|
|
||||||
|
echo "prev: $PREV_MONTH"
|
||||||
|
|
||||||
|
files=$(git log --since="$PREV_MONTH-01" --until="$PREV_MONTH-31" --name-only -- '**/CHANGELOG.md' | grep CHANGE | sort -u)
|
||||||
|
|
||||||
|
echo "files: $files"
|
||||||
|
|
||||||
|
echo "Below you can find the latest release for each individual package released during this month:"
|
||||||
|
echo
|
||||||
|
|
||||||
|
for file in $files; do
|
||||||
|
name=$(grep '^# ' $file | awk '{ print substr($0, 4) }')
|
||||||
|
last_release=$(grep '^## ' $file | awk '{ print substr($0, 4) }' | head -n 1)
|
||||||
|
echo "@$name: $last_release [CHANGELOG.md](https://github.com/nhost/nhost/blob/main/$file)"
|
||||||
|
done
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { navigateToProject } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { openProject } from '@/e2e/utils';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await openProject({
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
const AIRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/ai/assistants`;
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
await page.goto(AIRoute);
|
||||||
.getByRole('link', { name: /ai/i })
|
await page.waitForURL(AIRoute);
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { navigateToProject } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { openProject } from '@/e2e/utils';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await openProject({
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
const AIRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/ai/auto-embeddings`;
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
await page.goto(AIRoute);
|
||||||
.getByRole('link', { name: /ai/i })
|
await page.waitForURL(AIRoute);
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
|
|
||||||
test('should be able to ban and unban a user', async ({ page }) => {
|
test('should be able to ban and unban a user', async ({ page }) => {
|
||||||
await page.goto('/');
|
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||||
|
await page.goto(authUrl);
|
||||||
await openProject({
|
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||||
page,
|
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
|
||||||
.getByRole('link', { name: /auth/i })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForURL(
|
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const email = generateTestEmail();
|
const email = generateTestEmail();
|
||||||
const password = faker.internet.password();
|
const password = faker.internet.password();
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||||
|
await page.goto(authUrl);
|
||||||
await openProject({
|
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||||
page,
|
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
|
||||||
.getByRole('link', { name: /auth/i })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForURL(
|
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||||
|
await page.goto(authUrl);
|
||||||
await openProject({
|
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||||
page,
|
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
|
||||||
.getByRole('link', { name: /auth/i })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForURL(
|
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||||
|
await page.goto(authUrl);
|
||||||
await openProject({
|
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||||
page,
|
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
|
||||||
.getByRole('link', { name: /auth/i })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForURL(
|
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/users`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { navigateToProject, prepareTable } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { openProject, prepareTable } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
@@ -18,17 +14,15 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await openProject({
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
await page.goto(databaseRoute);
|
||||||
.getByRole('link', { name: /database/i })
|
await page.waitForURL(databaseRoute);
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
@@ -55,7 +49,7 @@ test('should create a simple table', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -84,7 +78,7 @@ test('should create a table with unique constraints', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -113,7 +107,7 @@ test('should create a table with nullable columns', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -146,7 +140,7 @@ test('should create a table with an identity column', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -174,7 +168,7 @@ test('should create table with foreign key constraint', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${firstTableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: /new table/i }).click();
|
await page.getByRole('button', { name: /new table/i }).click();
|
||||||
@@ -219,7 +213,7 @@ test('should create table with foreign key constraint', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${secondTableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -247,7 +241,7 @@ test('should not be able to create a table with a name that already exists', asy
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: /new table/i }).click();
|
await page.getByRole('button', { name: /new table/i }).click();
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
import { deleteTable, navigateToProject, prepareTable } from '@/e2e/utils';
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
|
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
@@ -18,17 +14,15 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await openProject({
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
await page.goto(databaseRoute);
|
||||||
.getByRole('link', { name: /database/i })
|
await page.waitForURL(databaseRoute);
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
@@ -53,7 +47,7 @@ test('should delete a table', async () => {
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await deleteTable({
|
await deleteTable({
|
||||||
@@ -63,7 +57,7 @@ test('should delete a table', async () => {
|
|||||||
|
|
||||||
// navigate to next URL
|
// navigate to next URL
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/**`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/**`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -91,7 +85,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${firstTableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: /new table/i }).click();
|
await page.getByRole('button', { name: /new table/i }).click();
|
||||||
@@ -138,7 +132,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
|||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(
|
await page.waitForURL(
|
||||||
`/${TEST_WORKSPACE_SLUG}/${PRO_TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${secondTableName}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import slugify from 'slugify';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL of the dashboard to test against.
|
* URL of the dashboard to test against.
|
||||||
*/
|
*/
|
||||||
@@ -8,15 +6,12 @@ export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL;
|
|||||||
/**
|
/**
|
||||||
* Name of the workspace to test against.
|
* Name of the workspace to test against.
|
||||||
*/
|
*/
|
||||||
export const TEST_WORKSPACE_NAME = process.env.NHOST_TEST_WORKSPACE_NAME;
|
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slugified name of the workspace to test against.
|
* Slug of the organization to test against.
|
||||||
*/
|
*/
|
||||||
export const TEST_WORKSPACE_SLUG = slugify(TEST_WORKSPACE_NAME, {
|
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG;
|
||||||
lower: true,
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the project to test against.
|
* Name of the project to test against.
|
||||||
@@ -24,25 +19,9 @@ export const TEST_WORKSPACE_SLUG = slugify(TEST_WORKSPACE_NAME, {
|
|||||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
|
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the pro test project to test against.
|
* Subdomain of the project to test against.
|
||||||
*/
|
*/
|
||||||
export const PRO_TEST_PROJECT_NAME = process.env.NHOST_PRO_TEST_PROJECT_NAME;
|
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN;
|
||||||
|
|
||||||
/**
|
|
||||||
* Slugified name of the project to test against.
|
|
||||||
*/
|
|
||||||
export const TEST_PROJECT_SLUG = slugify(TEST_PROJECT_NAME, {
|
|
||||||
lower: true,
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slugified name of the pro project to test against.
|
|
||||||
*/
|
|
||||||
export const PRO_TEST_PROJECT_SLUG = slugify(PRO_TEST_PROJECT_NAME, {
|
|
||||||
lower: true,
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hasura admin secret of the test project to use.
|
* Hasura admin secret of the test project to use.
|
||||||
@@ -59,3 +38,5 @@ export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL;
|
|||||||
* Password of the test account to use.
|
* Password of the test account to use.
|
||||||
*/
|
*/
|
||||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
|
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
|
||||||
|
|
||||||
|
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG;
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
TEST_PROJECT_NAME,
|
|
||||||
TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_NAME,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { openProject } from '@/e2e/utils';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { navigateToProject } from '../utils';
|
||||||
|
|
||||||
let page: Page;
|
let page: Page;
|
||||||
|
|
||||||
@@ -14,11 +9,11 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
page = await browser.newPage();
|
page = await browser.newPage();
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await openProject({
|
|
||||||
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,60 +21,34 @@ test.afterAll(async () => {
|
|||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show a sidebar with menu items', async () => {
|
test('should show the navtree with all links visible', async () => {
|
||||||
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
|
const navLocator = page.getByLabel('Navigation Tree');
|
||||||
await expect(navLocator).toBeVisible();
|
await expect(navLocator).toBeVisible();
|
||||||
await expect(navLocator.getByRole('list').getByRole('listitem')).toHaveCount(
|
|
||||||
13,
|
|
||||||
);
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /overview/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /database/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /graphql/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(navLocator.getByRole('link', { name: /hasura/i })).toBeVisible();
|
|
||||||
await expect(navLocator.getByRole('link', { name: /auth/i })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /storage/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /deployments/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /backups/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(navLocator.getByRole('link', { name: /logs/i })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /metrics/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
navLocator.getByRole('link', { name: /settings/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show a header with a logo, the workspace name, and the project name', async () => {
|
const links = [
|
||||||
await expect(
|
'Nhost Automation Test Project',
|
||||||
page.getByRole('banner').getByRole('link', { name: TEST_WORKSPACE_NAME }),
|
'Overview',
|
||||||
).toBeVisible();
|
'Database',
|
||||||
|
'GraphQL',
|
||||||
|
'Hasura',
|
||||||
|
'Auth',
|
||||||
|
'Storage',
|
||||||
|
'Run',
|
||||||
|
'AI',
|
||||||
|
'Deployments',
|
||||||
|
'Backups',
|
||||||
|
'Logs',
|
||||||
|
'Metrics',
|
||||||
|
'Settings',
|
||||||
|
];
|
||||||
|
|
||||||
await expect(
|
for (const linkName of links) {
|
||||||
page.getByRole('banner').getByRole('link', { name: TEST_PROJECT_NAME }),
|
const link =
|
||||||
).toBeVisible();
|
linkName === 'Settings'
|
||||||
});
|
? page.getByRole('link', { name: linkName }).first()
|
||||||
|
: page.getByRole('link', { name: linkName });
|
||||||
test("should show the project's name, the Upgrade button and the Settings button", async () => {
|
await expect(link).toBeVisible();
|
||||||
await expect(
|
}
|
||||||
page.getByRole('heading', { name: TEST_PROJECT_NAME }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.getByText(/starter/i)).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: /upgrade/i })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('main').getByRole('link', { name: /settings/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show the project's region and subdomain", async () => {
|
test("should show the project's region and subdomain", async () => {
|
||||||
@@ -96,27 +65,3 @@ test('should not have a GitHub repository connected', async () => {
|
|||||||
page.getByRole('button', { name: /connect to github/i }).first(),
|
page.getByRole('button', { name: /connect to github/i }).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show metrics', async () => {
|
|
||||||
await expect(page.getByText(/cpu usage seconds\d+/i)).toBeVisible();
|
|
||||||
await expect(page.getByText(/total requests\d+/i)).toBeVisible();
|
|
||||||
await expect(page.getByText(/function invocations\d+/i)).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByText(/egress volume\d+(\.\d+)? [a-zA-Z]+/i),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.getByText(/logs\d+(\.\d+)? [a-zA-Z]+/i)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show proper limits for the free project', async () => {
|
|
||||||
await expect(
|
|
||||||
page.getByText(/database\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByText(/storage\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByText(/users[0-9]+ of [0-9]+/i)).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByText(/functions[0-9]+ of [0-9]+/i)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import {
|
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||||
PRO_TEST_PROJECT_NAME,
|
|
||||||
PRO_TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
|
||||||
import { openProject } from '@/e2e/utils';
|
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { navigateToProject } from '../utils';
|
||||||
|
|
||||||
let page: Page;
|
let page: Page;
|
||||||
|
|
||||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await openProject({
|
await navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName: PRO_TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
const runRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/run`;
|
||||||
.getByRole('navigation', { name: /main navigation/i })
|
await page.goto(runRoute);
|
||||||
.getByRole('link', { name: /run/i })
|
await page.waitForURL(runRoute);
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
@@ -39,6 +33,10 @@ test('should create and delete a run service', async () => {
|
|||||||
await page.getByPlaceholder(/service name/i).click();
|
await page.getByPlaceholder(/service name/i).click();
|
||||||
await page.getByPlaceholder(/service name/i).fill('test');
|
await page.getByPlaceholder(/service name/i).fill('test');
|
||||||
|
|
||||||
|
await page.getByText('Nhost registry').click();
|
||||||
|
await page.getByPlaceholder('Replicas').click();
|
||||||
|
await page.getByPlaceholder('Replicas').fill('0');
|
||||||
|
|
||||||
await page.getByRole('button', { name: /create/i }).click();
|
await page.getByRole('button', { name: /create/i }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -47,12 +45,6 @@ test('should create and delete a run service', async () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: /confirm/i }).click();
|
await page.getByRole('button', { name: /confirm/i }).click();
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('heading', { name: /service details/i }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: /ok/i }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel(/more options/i).click();
|
await page.getByLabel(/more options/i).click();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
TEST_DASHBOARD_URL,
|
TEST_DASHBOARD_URL,
|
||||||
|
TEST_PERSONAL_ORG_SLUG,
|
||||||
TEST_USER_EMAIL,
|
TEST_USER_EMAIL,
|
||||||
TEST_USER_PASSWORD,
|
TEST_USER_PASSWORD,
|
||||||
} from '@/e2e/env';
|
} from '@/e2e/env';
|
||||||
@@ -15,6 +16,9 @@ setup('authenticate user', async ({ page }) => {
|
|||||||
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
||||||
await page.getByRole('button', { name: /sign in/i }).click();
|
await page.getByRole('button', { name: /sign in/i }).click();
|
||||||
|
|
||||||
await page.waitForURL(TEST_DASHBOARD_URL);
|
await page.waitForURL(
|
||||||
|
`${TEST_DASHBOARD_URL}/orgs/${TEST_PERSONAL_ORG_SLUG}/projects`,
|
||||||
|
{ waitUntil: 'networkidle' },
|
||||||
|
);
|
||||||
await page.context().storageState({ path: 'e2e/.auth/user.json' });
|
await page.context().storageState({ path: 'e2e/.auth/user.json' });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,24 +5,27 @@ import type { Page } from '@playwright/test';
|
|||||||
* Open a project by navigating to the project's overview page.
|
* Open a project by navigating to the project's overview page.
|
||||||
*
|
*
|
||||||
* @param page - The Playwright page object.
|
* @param page - The Playwright page object.
|
||||||
* @param workspaceSlug - The slug of the workspace that contains the project.
|
* @param orgSlug - The slug of the organization that contains the project.
|
||||||
* @param projectSlug - The slug of the project to open.
|
* @param projectSubdomain - The subdomain of the project to open.
|
||||||
* @param projectName - The name of the project to open.
|
|
||||||
* @returns A promise that resolves when the project is opened.
|
* @returns A promise that resolves when the project is opened.
|
||||||
*/
|
*/
|
||||||
export async function openProject({
|
export async function navigateToProject({
|
||||||
page,
|
page,
|
||||||
projectName,
|
orgSlug,
|
||||||
workspaceSlug,
|
projectSubdomain,
|
||||||
projectSlug,
|
|
||||||
}: {
|
}: {
|
||||||
page: Page;
|
page: Page;
|
||||||
workspaceSlug: string;
|
orgSlug: string;
|
||||||
projectSlug: string;
|
projectSubdomain: string;
|
||||||
projectName: string;
|
|
||||||
}) {
|
}) {
|
||||||
await page.getByRole('link', { name: projectName }).click();
|
const projectUrl = `/orgs/${orgSlug}/projects/${projectSubdomain}`;
|
||||||
await page.waitForURL(`/${workspaceSlug}/${projectSlug}`);
|
|
||||||
|
try {
|
||||||
|
await page.goto(projectUrl, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForURL(projectUrl, { timeout: 10000 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to navigate to project URL: ${projectUrl}`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
TEST_DASHBOARD_URL,
|
TEST_DASHBOARD_URL,
|
||||||
|
TEST_ORGANIZATION_SLUG,
|
||||||
TEST_PROJECT_ADMIN_SECRET,
|
TEST_PROJECT_ADMIN_SECRET,
|
||||||
TEST_PROJECT_NAME,
|
TEST_PROJECT_SUBDOMAIN,
|
||||||
TEST_PROJECT_SLUG,
|
|
||||||
TEST_WORKSPACE_SLUG,
|
|
||||||
} from '@/e2e/env';
|
} from '@/e2e/env';
|
||||||
import { openProject } from '@/e2e/utils';
|
import { navigateToProject } from '@/e2e/utils';
|
||||||
import { chromium } from '@playwright/test';
|
import { chromium } from '@playwright/test';
|
||||||
|
|
||||||
async function globalTeardown() {
|
async function globalTeardown() {
|
||||||
@@ -18,13 +17,10 @@ async function globalTeardown() {
|
|||||||
|
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
await page.goto('/');
|
await navigateToProject({
|
||||||
|
|
||||||
await openProject({
|
|
||||||
page,
|
page,
|
||||||
projectName: TEST_PROJECT_NAME,
|
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||||
projectSlug: TEST_PROJECT_SLUG,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pagePromise = context.waitForEvent('page');
|
const pagePromise = context.waitForEvent('page');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "2.1.3",
|
"version": "2.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@mui/x-date-pickers": "^5.0.20",
|
"@mui/x-date-pickers": "^5.0.20",
|
||||||
"@nhost/nextjs": "workspace:*",
|
"@nhost/nextjs": "workspace:*",
|
||||||
"@nhost/react-apollo": "workspace:*",
|
"@nhost/react-apollo": "workspace:*",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
@@ -127,7 +128,7 @@
|
|||||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||||
"@next/bundle-analyzer": "^12.3.4",
|
"@next/bundle-analyzer": "^12.3.4",
|
||||||
"@playwright/test": "1.41.0",
|
"@playwright/test": "1.47.0",
|
||||||
"@storybook/addon-actions": "^6.5.16",
|
"@storybook/addon-actions": "^6.5.16",
|
||||||
"@storybook/addon-essentials": "^6.5.16",
|
"@storybook/addon-essentials": "^6.5.16",
|
||||||
"@storybook/addon-interactions": "^6.5.16",
|
"@storybook/addon-interactions": "^6.5.16",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
timeout: 30 * 1000,
|
timeout: 40 * 1000,
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
},
|
},
|
||||||
@@ -20,6 +20,9 @@ export default defineConfig({
|
|||||||
actionTimeout: 0,
|
actionTimeout: 0,
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
|
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
|
||||||
|
launchOptions: {
|
||||||
|
slowMo: 500,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { LocalAccountMenu } from '@/components/layout/LocalAccountMenu';
|
|||||||
import { MobileNav } from '@/components/layout/MobileNav';
|
import { MobileNav } from '@/components/layout/MobileNav';
|
||||||
import { Logo } from '@/components/presentational/Logo';
|
import { Logo } from '@/components/presentational/Logo';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Box } from '@/components/ui/v2/Box';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
|
||||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
import { DevAssistant as WorkspaceProjectDevAssistant } from '@/features/ai/DevAssistant';
|
import { DevAssistant as WorkspaceProjectDevAssistant } from '@/features/ai/DevAssistant';
|
||||||
import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray';
|
import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray';
|
||||||
import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray';
|
import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray';
|
||||||
@@ -74,8 +74,12 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
<BreadcrumbNav />
|
<BreadcrumbNav />
|
||||||
|
|
||||||
<div className="hidden grid-flow-col items-center gap-1 sm:grid">
|
<div className="hidden grid-flow-col items-center gap-1 sm:grid">
|
||||||
<Button className="rounded-full" onClick={openDevAssistant}>
|
<Button
|
||||||
<GraphiteIcon className="h-4 w-4" />
|
variant="outline"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={openDevAssistant}
|
||||||
|
>
|
||||||
|
<GraphiteIcon className="h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<NotificationsTray />
|
<NotificationsTray />
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function OrgsComboBox() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-between gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
className="justify-between w-full gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
||||||
>
|
>
|
||||||
{selectedItem ? (
|
{selectedItem ? (
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="flex flex-row items-center justify-center">
|
||||||
@@ -115,7 +115,7 @@ export default function OrgsComboBox() {
|
|||||||
) : (
|
) : (
|
||||||
'Select organization / workspace'
|
'Select organization / workspace'
|
||||||
)}
|
)}
|
||||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
<ChevronsUpDown className="w-5 h-5 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" side="bottom" align="start">
|
<PopoverContent className="p-0" side="bottom" align="start">
|
||||||
@@ -129,7 +129,7 @@ export default function OrgsComboBox() {
|
|||||||
keywords={[option.label]}
|
keywords={[option.label]}
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className="flex items-center justify-between bg-background text-foreground dark:hover:bg-muted"
|
className="flex items-center text-foreground dark:hover:bg-muted"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setSelectedItem(option);
|
setSelectedItem(option);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -144,17 +144,15 @@ export default function OrgsComboBox() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center font-normal">
|
<Check
|
||||||
<Check
|
className={cn(
|
||||||
className={cn(
|
'mr-2 h-4 w-4',
|
||||||
'mr-2 h-4 w-4',
|
selectedItem?.value === option.value
|
||||||
selectedItem?.value === option.value
|
? 'opacity-100'
|
||||||
? 'opacity-100'
|
: 'opacity-0',
|
||||||
: 'opacity-0',
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<span className="w-full truncate">{option.label}</span>
|
||||||
<span className="max-w-52 truncate">{option.label}</span>
|
|
||||||
</div>
|
|
||||||
{renderBadge(option.plan)}
|
{renderBadge(option.plan)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
@@ -170,7 +168,7 @@ export default function OrgsComboBox() {
|
|||||||
keywords={[option.label]}
|
keywords={[option.label]}
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
className="flex items-center justify-between bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
className="flex items-center text-foreground dark:hover:bg-muted"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setSelectedItem(option);
|
setSelectedItem(option);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -185,19 +183,15 @@ export default function OrgsComboBox() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<Check
|
||||||
<Check
|
className={cn(
|
||||||
className={cn(
|
'mr-2 h-4 w-4',
|
||||||
'mr-2 h-4 w-4',
|
selectedItem?.value === option.value
|
||||||
selectedItem?.value === option.value
|
? 'opacity-100'
|
||||||
? 'opacity-100'
|
: 'opacity-0',
|
||||||
: 'opacity-0',
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<span className="w-full truncate">{option.label}</span>
|
||||||
<span className="max-w-52 truncate">
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{renderBadge(option.plan)}
|
{renderBadge(option.plan)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ const projectSettingsPages = [
|
|||||||
slug: 'authentication',
|
slug: 'authentication',
|
||||||
route: 'authentication',
|
route: 'authentication',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'JWT',
|
||||||
|
slug: 'jwt',
|
||||||
|
route: 'jwt',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Sign-In methods',
|
name: 'Sign-In methods',
|
||||||
slug: 'sign-in-methods',
|
slug: 'sign-in-methods',
|
||||||
@@ -68,6 +73,7 @@ const projectSettingsPages = [
|
|||||||
route: 'rate-limiting',
|
route: 'rate-limiting',
|
||||||
},
|
},
|
||||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||||
|
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||||
].map((item) => ({
|
].map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@@ -125,7 +131,7 @@ export default function ProjectSettingsPagesComboBox() {
|
|||||||
) : (
|
) : (
|
||||||
<>Select a page</>
|
<>Select a page</>
|
||||||
)}
|
)}
|
||||||
<ChevronsUpDown className="w-5 h-5 text-muted-foreground" />
|
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" side="bottom" align="start">
|
<PopoverContent className="p-0" side="bottom" align="start">
|
||||||
@@ -155,7 +161,7 @@ export default function ProjectSettingsPagesComboBox() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="truncate max-w-52">{option.label}</span>
|
<span className="max-w-52 truncate">{option.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,13 +8,20 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from '@/components/ui/v3/command';
|
} from '@/components/ui/v3/command';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/v3/hover-card';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/v3/popover';
|
} from '@/components/ui/v3/popover';
|
||||||
|
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -24,17 +31,76 @@ type Option = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function ProjectStatusIndicator({ status }: { status: ApplicationStatus }) {
|
||||||
|
const indicatorStyles: Record<
|
||||||
|
number,
|
||||||
|
{ className: string; description: string }
|
||||||
|
> = {
|
||||||
|
[ApplicationStatus.Errored]: {
|
||||||
|
className: 'bg-destructive',
|
||||||
|
description: 'Project errored',
|
||||||
|
},
|
||||||
|
[ApplicationStatus.Pausing]: {
|
||||||
|
className: 'bg-primary-main animate-blinking',
|
||||||
|
description: 'Project is pausing',
|
||||||
|
},
|
||||||
|
[ApplicationStatus.Restoring]: {
|
||||||
|
className: 'bg-primary-main animate-blinking',
|
||||||
|
description: 'Project is restoring',
|
||||||
|
},
|
||||||
|
[ApplicationStatus.Paused]: {
|
||||||
|
className: 'bg-slate-400',
|
||||||
|
description: 'Project is paused',
|
||||||
|
},
|
||||||
|
[ApplicationStatus.Unpausing]: {
|
||||||
|
className: 'bg-primary-main animate-blinking',
|
||||||
|
description: 'Project is unpausing',
|
||||||
|
},
|
||||||
|
[ApplicationStatus.Live]: {
|
||||||
|
className: 'bg-primary-main',
|
||||||
|
description: 'Project is live',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const style = indicatorStyles[status];
|
||||||
|
|
||||||
|
if (style) {
|
||||||
|
return (
|
||||||
|
<HoverCard openDelay={0}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<span
|
||||||
|
className={cn('mt-[1px] h-2 w-2 rounded-full', style.className)}
|
||||||
|
/>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="top" className="h-fit w-fit py-2">
|
||||||
|
{style.description}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProjectsComboBox() {
|
export default function ProjectsComboBox() {
|
||||||
const {
|
const {
|
||||||
query: { appSubdomain },
|
query: { appSubdomain },
|
||||||
push,
|
push,
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
|
|
||||||
|
const { state: appState } = useAppState();
|
||||||
const { currentOrg: { slug: orgSlug, apps = [] } = {} } = useOrgs();
|
const { currentOrg: { slug: orgSlug, apps = [] } = {} } = useOrgs();
|
||||||
const selectedProjectFromUrl = apps.find(
|
|
||||||
(item) => item.subdomain === appSubdomain,
|
|
||||||
);
|
|
||||||
const [selectedProject, setSelectedProject] = useState<Option | null>(null);
|
const [selectedProject, setSelectedProject] = useState<Option | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const options: Option[] = apps.map((app) => ({
|
||||||
|
label: app.name,
|
||||||
|
value: app.subdomain,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedProjectFromUrl = apps.find(
|
||||||
|
(app) => app.subdomain === appSubdomain,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProjectFromUrl) {
|
if (selectedProjectFromUrl) {
|
||||||
@@ -45,68 +111,64 @@ export default function ProjectsComboBox() {
|
|||||||
}
|
}
|
||||||
}, [selectedProjectFromUrl]);
|
}, [selectedProjectFromUrl]);
|
||||||
|
|
||||||
const options: Option[] = apps.map((app) => ({
|
const handleProjectSelect = (option: Option) => {
|
||||||
label: app.name,
|
setSelectedProject(option);
|
||||||
value: app.subdomain,
|
setOpen(false);
|
||||||
}));
|
push(`/orgs/${orgSlug}/projects/${option.value}`);
|
||||||
|
};
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<div className="flex items-center gap-1">
|
||||||
<PopoverTrigger asChild>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<Button
|
<PopoverTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="justify-start gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
size="sm"
|
||||||
>
|
className="justify-start gap-2 bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
||||||
{selectedProject ? (
|
>
|
||||||
<div className="flex flex-row items-center justify-center gap-1">
|
{selectedProject ? (
|
||||||
<Box className="h-4 w-4" />
|
<div className="flex items-center gap-2">
|
||||||
{selectedProject.label}
|
<ProjectStatusIndicator status={appState} />
|
||||||
<ProjectStatus />
|
{selectedProject.label}
|
||||||
</div>
|
<ProjectStatus />
|
||||||
) : (
|
</div>
|
||||||
<>Select a project</>
|
) : (
|
||||||
)}
|
<>Select a project</>
|
||||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
)}
|
||||||
</Button>
|
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||||
</PopoverTrigger>
|
</Button>
|
||||||
<PopoverContent className="p-0" side="bottom" align="start">
|
</PopoverTrigger>
|
||||||
<Command>
|
<PopoverContent className="p-0" side="bottom" align="start">
|
||||||
<CommandInput placeholder="Select a project..." />
|
<Command>
|
||||||
<CommandList>
|
<CommandInput placeholder="Select a project..." />
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandList>
|
||||||
<CommandGroup>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
{options.map((option) => (
|
<CommandGroup>
|
||||||
<CommandItem
|
{options.map((option) => (
|
||||||
keywords={[option.label]}
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onSelect={() => {
|
onSelect={() => handleProjectSelect(option)}
|
||||||
setSelectedProject(option);
|
>
|
||||||
setOpen(false);
|
<Check
|
||||||
push(`/orgs/${orgSlug}/projects/${option.value}`);
|
className={cn(
|
||||||
}}
|
'mr-2 h-4 w-4',
|
||||||
>
|
selectedProject?.value === option.value
|
||||||
<Check
|
? 'opacity-100'
|
||||||
className={cn(
|
: 'opacity-0',
|
||||||
'mr-2 h-4 w-4',
|
)}
|
||||||
selectedProject?.value === option.value
|
/>
|
||||||
? 'opacity-100'
|
<div className="flex items-center gap-1">
|
||||||
: 'opacity-0',
|
<Box className="h-4 w-4" />
|
||||||
)}
|
<span className="max-w-52 truncate">{option.label}</span>
|
||||||
/>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-1">
|
</CommandItem>
|
||||||
<Box className="h-4 w-4" />
|
))}
|
||||||
<span className="max-w-52 truncate">{option.label}</span>
|
</CommandGroup>
|
||||||
</div>
|
</CommandList>
|
||||||
</CommandItem>
|
</Command>
|
||||||
))}
|
</PopoverContent>
|
||||||
</CommandGroup>
|
</Popover>
|
||||||
</CommandList>
|
</div>
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,73 +30,73 @@ import { useTreeNavState } from './TreeNavStateContext';
|
|||||||
const projectPages = [
|
const projectPages = [
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
icon: <HomeIcon className="w-4 h-4" />,
|
icon: <HomeIcon className="h-4 w-4" />,
|
||||||
route: '',
|
route: '',
|
||||||
slug: 'overview',
|
slug: 'overview',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Database',
|
name: 'Database',
|
||||||
icon: <DatabaseIcon className="w-4 h-4" />,
|
icon: <DatabaseIcon className="h-4 w-4" />,
|
||||||
route: 'database/browser/default',
|
route: 'database/browser/default',
|
||||||
slug: 'database',
|
slug: 'database',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'GraphQL',
|
name: 'GraphQL',
|
||||||
icon: <GraphQLIcon className="w-4 h-4" />,
|
icon: <GraphQLIcon className="h-4 w-4" />,
|
||||||
route: 'graphql',
|
route: 'graphql',
|
||||||
slug: 'graphql',
|
slug: 'graphql',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hasura',
|
name: 'Hasura',
|
||||||
icon: <HasuraIcon className="w-4 h-4" />,
|
icon: <HasuraIcon className="h-4 w-4" />,
|
||||||
route: 'hasura',
|
route: 'hasura',
|
||||||
slug: 'hasura',
|
slug: 'hasura',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Auth',
|
name: 'Auth',
|
||||||
icon: <UserIcon className="w-4 h-4" />,
|
icon: <UserIcon className="h-4 w-4" />,
|
||||||
route: 'users',
|
route: 'users',
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Storage',
|
name: 'Storage',
|
||||||
icon: <StorageIcon className="w-4 h-4" />,
|
icon: <StorageIcon className="h-4 w-4" />,
|
||||||
route: 'storage',
|
route: 'storage',
|
||||||
slug: 'storage',
|
slug: 'storage',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Run',
|
name: 'Run',
|
||||||
icon: <ServicesIcon className="w-4 h-4" />,
|
icon: <ServicesIcon className="h-4 w-4" />,
|
||||||
route: 'run',
|
route: 'run',
|
||||||
slug: 'run',
|
slug: 'run',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'AI',
|
name: 'AI',
|
||||||
icon: <AIIcon className="w-4 h-4" />,
|
icon: <AIIcon className="h-4 w-4" />,
|
||||||
route: 'ai/auto-embeddings',
|
route: 'ai/auto-embeddings',
|
||||||
slug: 'ai',
|
slug: 'ai',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Deployments',
|
name: 'Deployments',
|
||||||
icon: <RocketIcon className="w-4 h-4" />,
|
icon: <RocketIcon className="h-4 w-4" />,
|
||||||
route: 'deployments',
|
route: 'deployments',
|
||||||
slug: 'deployments',
|
slug: 'deployments',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Backups',
|
name: 'Backups',
|
||||||
icon: <CloudIcon className="w-4 h-4" />,
|
icon: <CloudIcon className="h-4 w-4" />,
|
||||||
route: 'backups',
|
route: 'backups',
|
||||||
slug: 'backups',
|
slug: 'backups',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
icon: <FileTextIcon className="w-4 h-4" />,
|
icon: <FileTextIcon className="h-4 w-4" />,
|
||||||
route: 'logs',
|
route: 'logs',
|
||||||
slug: 'logs',
|
slug: 'logs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Metrics',
|
name: 'Metrics',
|
||||||
icon: <GaugeIcon className="w-4 h-4" />,
|
icon: <GaugeIcon className="h-4 w-4" />,
|
||||||
route: 'metrics',
|
route: 'metrics',
|
||||||
slug: 'metrics',
|
slug: 'metrics',
|
||||||
},
|
},
|
||||||
@@ -121,6 +121,11 @@ const projectSettingsPages = [
|
|||||||
slug: 'authentication',
|
slug: 'authentication',
|
||||||
route: 'authentication',
|
route: 'authentication',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'JWT',
|
||||||
|
slug: 'jwt',
|
||||||
|
route: 'jwt',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Sign-In methods',
|
name: 'Sign-In methods',
|
||||||
slug: 'sign-in-methods',
|
slug: 'sign-in-methods',
|
||||||
@@ -151,6 +156,7 @@ const projectSettingsPages = [
|
|||||||
route: 'rate-limiting',
|
route: 'rate-limiting',
|
||||||
},
|
},
|
||||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||||
|
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -203,7 +209,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
|||||||
data: {
|
data: {
|
||||||
name: 'New project',
|
name: 'New project',
|
||||||
slug: 'new',
|
slug: 'new',
|
||||||
icon: <Plus className="w-4 h-4 mr-1 font-bold" strokeWidth={3} />,
|
icon: <Plus className="mr-1 h-4 w-4 font-bold" strokeWidth={3} />,
|
||||||
targetUrl: `/orgs/${org.slug}/projects/new`,
|
targetUrl: `/orgs/${org.slug}/projects/new`,
|
||||||
disabled: !isPlatform,
|
disabled: !isPlatform,
|
||||||
},
|
},
|
||||||
@@ -218,7 +224,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
|||||||
data: {
|
data: {
|
||||||
name: app.name,
|
name: app.name,
|
||||||
slug: app.subdomain,
|
slug: app.subdomain,
|
||||||
icon: <Box className="w-4 h-4" />,
|
icon: <Box className="h-4 w-4" />,
|
||||||
targetUrl: `/orgs/${org.slug}/projects/${app.subdomain}`,
|
targetUrl: `/orgs/${org.slug}/projects/${app.subdomain}`,
|
||||||
},
|
},
|
||||||
children: projectPages.map(
|
children: projectPages.map(
|
||||||
@@ -397,9 +403,9 @@ export default function NavTree() {
|
|||||||
className="h-8 px-1"
|
className="h-8 px-1"
|
||||||
>
|
>
|
||||||
{context.isExpanded ? (
|
{context.isExpanded ? (
|
||||||
<ChevronDown className="w-4 h-4 font-bold" strokeWidth={3} />
|
<ChevronDown className="h-4 w-4 font-bold" strokeWidth={3} />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="w-4 h-4" strokeWidth={3} />
|
<ChevronRight className="h-4 w-4" strokeWidth={3} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -491,9 +497,9 @@ export default function NavTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row w-full">
|
<div className="flex w-full flex-row">
|
||||||
<div className="flex justify-center px-[12px] pb-3">
|
<div className="flex justify-center px-[12px] pb-3">
|
||||||
<div className="w-0 h-full border-r border-dashed" />
|
<div className="h-full w-0 border-r border-dashed" />
|
||||||
</div>
|
</div>
|
||||||
<ul {...containerProps} className="w-full">
|
<ul {...containerProps} className="w-full">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{ backgroundColor: 'background.default' }}
|
sx={{ backgroundColor: 'background.default' }}
|
||||||
className="flex w-full flex-auto flex-col overflow-y-auto overflow-x-hidden"
|
className="flex flex-col flex-auto w-full overflow-x-hidden overflow-y-auto"
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{ backgroundColor: 'background.default' }}
|
sx={{ backgroundColor: 'background.default' }}
|
||||||
className="flex h-full flex-col"
|
className="flex flex-col h-full"
|
||||||
>
|
>
|
||||||
<RetryableErrorBoundary>
|
<RetryableErrorBoundary>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{hasGitRepo && (
|
{hasGitRepo && (
|
||||||
<Alert
|
<Alert
|
||||||
severity="warning"
|
severity="warning"
|
||||||
className="grid grid-flow-row place-content-center gap-2"
|
className="grid grid-flow-row gap-2 place-content-center"
|
||||||
>
|
>
|
||||||
<Text color="warning" className="text-sm">
|
<Text color="warning" className="text-sm">
|
||||||
As you have a connected repository, make sure to synchronize
|
As you have a connected repository, make sure to synchronize
|
||||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="underline"
|
||||||
href="https://docs.nhost.io/cli/overlays"
|
href="https://docs.nhost.io/guides/cli/configuration-overlays#configuration-overlays"
|
||||||
>
|
>
|
||||||
docs.nhost.io/cli/overlays
|
Configuration Overlays
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for guidance.
|
for guidance.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
58
dashboard/src/components/ui/v3/accordion.tsx
Normal file
58
dashboard/src/components/ui/v3/accordion.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn('border-b', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = 'AccordionItem';
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
));
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
));
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
@@ -13,7 +13,7 @@ const buttonVariants = cva(
|
|||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
'border bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
|||||||
@@ -3,20 +3,30 @@ import * as React from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||||
|
prefix?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, prefix, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<div className="relative flex items-center">
|
||||||
type={type}
|
{prefix && (
|
||||||
className={cn(
|
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
||||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
{prefix}
|
||||||
className,
|
</span>
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
<input
|
||||||
{...props}
|
type={type}
|
||||||
/>
|
className={cn(
|
||||||
|
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
||||||
|
prefix && 'pl-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
|||||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
@@ -30,7 +29,6 @@ export default function DisableNewUsersSettings() {
|
|||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,14 +40,14 @@ export default function DisableNewUsersSettings() {
|
|||||||
const form = useForm<DisableNewUsersFormValues>({
|
const form = useForm<DisableNewUsersFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
form.reset({
|
form.reset({
|
||||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loading, data, form]);
|
}, [loading, data, form]);
|
||||||
@@ -58,7 +56,7 @@ export default function DisableNewUsersSettings() {
|
|||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading disabled sign up settings..."
|
label="Loading disabled new users settings..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -79,7 +77,7 @@ export default function DisableNewUsersSettings() {
|
|||||||
config: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
signUp: {
|
signUp: {
|
||||||
enabled: !values.disabled,
|
disableNewUsers: values.disabled,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,10 +102,10 @@ export default function DisableNewUsersSettings() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loadingMessage: 'Disabling new user sign ups...',
|
loadingMessage: 'Disabling new users sign ins...',
|
||||||
successMessage: 'New user sign ups have been disabled successfully.',
|
successMessage: 'New users sign ins have been disabled successfully.',
|
||||||
errorMessage:
|
errorMessage:
|
||||||
'An error occurred while trying to disable new user sign ups.',
|
'An error occurred while trying to disable new users sign ins.',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ query GetAuthenticationSettings($appId: uuid!) {
|
|||||||
}
|
}
|
||||||
signUp {
|
signUp {
|
||||||
enabled
|
enabled
|
||||||
|
disableNewUsers
|
||||||
}
|
}
|
||||||
session {
|
session {
|
||||||
accessToken {
|
accessToken {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Divider } from '@/components/ui/v2/Divider';
|
||||||
|
import { BillingCycle } from './components/BillingCycle';
|
||||||
|
import { BillingDetails } from './components/BillingDetails';
|
||||||
|
import { Estimate } from './components/Estimate';
|
||||||
|
import { SpendingNotifications } from './components/SpendingNotifications';
|
||||||
|
|
||||||
|
export default function BillingEstimate() {
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<div className="flex w-full flex-col rounded-md border bg-background">
|
||||||
|
<div className="flex w-full flex-col gap-1 p-4">
|
||||||
|
<span className="text-xl font-medium">Billing Estimate</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Divider />
|
||||||
|
<BillingCycle />
|
||||||
|
<Divider />
|
||||||
|
<Estimate />
|
||||||
|
<Divider />
|
||||||
|
<SpendingNotifications />
|
||||||
|
<Divider />
|
||||||
|
<BillingDetails />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Progress } from '@/components/ui/v3/progress';
|
||||||
|
import { getBillingCycleInfo } from '@/features/orgs/components/billing/utils/getBillingCycle';
|
||||||
|
|
||||||
|
export default function BillingCycle() {
|
||||||
|
const { progress, billingCycleStart, billingCycleEnd, daysLeft } =
|
||||||
|
getBillingCycleInfo();
|
||||||
|
|
||||||
|
const daysText = daysLeft === 1 ? 'day' : 'days';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col justify-between gap-4 p-4 md:flex-row md:gap-8">
|
||||||
|
<div className="flex basis-1/2 flex-col">
|
||||||
|
<span className="font-medium">
|
||||||
|
Current billing cycle ({daysLeft} {daysText} left)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-2 pb-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">{billingCycleStart}</span>
|
||||||
|
<span className="text-muted-foreground">{billingCycleEnd}</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={progress} className="h-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as BillingCycle } from './BillingCycle';
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/ui/v3/accordion';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/v3/table';
|
||||||
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
|
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
||||||
|
|
||||||
|
export default function BillingDetails() {
|
||||||
|
const { org } = useCurrentOrg();
|
||||||
|
const { data, loading } = useBillingGetNextInvoiceQuery({
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
variables: {
|
||||||
|
organizationID: org?.id,
|
||||||
|
},
|
||||||
|
skip: !org,
|
||||||
|
});
|
||||||
|
|
||||||
|
const billingItems = data?.billingGetNextInvoice?.items ?? [];
|
||||||
|
const amountDue = data?.billingGetNextInvoice?.AmountDue ?? null;
|
||||||
|
|
||||||
|
if (!data || loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<div className="flex h-32 place-content-center">
|
||||||
|
<ActivityIndicator
|
||||||
|
label="Loading billing details..."
|
||||||
|
className="justify-center text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion type="single" collapsible>
|
||||||
|
<AccordionItem value="details" className="border-none">
|
||||||
|
<AccordionTrigger className="p-4">Details</AccordionTrigger>
|
||||||
|
<AccordionContent className="border-t-1 pb-0">
|
||||||
|
<div className="rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="w-full bg-accent">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
||||||
|
Item
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="rounded-tr-md text-right">
|
||||||
|
Amount
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{billingItems.map((billingItem) => (
|
||||||
|
<TableRow key={billingItem.Description}>
|
||||||
|
<TableCell colSpan={3}>{billingItem.Description}</TableCell>
|
||||||
|
<TableCell colSpan={3} className="text-right">
|
||||||
|
${billingItem.Amount}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter className="bg-accent">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="rounded-bl-md">
|
||||||
|
Total
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="rounded-br-md text-right">
|
||||||
|
${amountDue}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as BillingDetails } from './BillingDetails';
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
|
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export default function Estimate() {
|
||||||
|
const { org } = useCurrentOrg();
|
||||||
|
const { data, loading } = useBillingGetNextInvoiceQuery({
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
variables: {
|
||||||
|
organizationID: org?.id,
|
||||||
|
},
|
||||||
|
skip: !org,
|
||||||
|
});
|
||||||
|
|
||||||
|
const amountDue = useMemo(() => {
|
||||||
|
const amount = data?.billingGetNextInvoice?.AmountDue;
|
||||||
|
if (!amount) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
if (typeof amount !== 'number') {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
return amount.toLocaleString('en', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col justify-between gap-2 p-4 md:flex-row md:gap-8">
|
||||||
|
<div className="flex basis-1/2 flex-col">
|
||||||
|
<span className="font-medium">Estimate</span>
|
||||||
|
<span className="text-xl font-semibold">${amountDue}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<p className="max-w-prose">
|
||||||
|
This estimate reflects your estimated next invoice based on current
|
||||||
|
usage. Please note that usage data may have a processing delay of a
|
||||||
|
few hours.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Estimate } from './Estimate';
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
|
import { Switch } from '@/components/ui/v2/Switch';
|
||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/v3/form';
|
||||||
|
import { Input } from '@/components/ui/v3/input';
|
||||||
|
import { Progress } from '@/components/ui/v3/progress';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/v3/tooltip';
|
||||||
|
import { useIsOrgAdmin } from '@/features/orgs/hooks/useIsOrgAdmin';
|
||||||
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
import {
|
||||||
|
GetOrganizationSpendingNotificationDocument,
|
||||||
|
useBillingGetNextInvoiceQuery,
|
||||||
|
useGetOrganizationSpendingNotificationQuery,
|
||||||
|
useUpdateOrganizationSpendingNotificationMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { useEffect, useMemo, type ChangeEvent } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
enabled: Yup.boolean().required(),
|
||||||
|
threshold: Yup.number().test(
|
||||||
|
'is-valid-threshold',
|
||||||
|
`Threshold must be greater than 110% of your plan's price`,
|
||||||
|
(value: number, { options }) => {
|
||||||
|
const planPrice = options?.context?.planPrice || 0;
|
||||||
|
if (value === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof value === 'number' && value > 1.1 * planPrice) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type SpendingNotificationsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
|
export default function SpendingNotifications() {
|
||||||
|
const { org } = useCurrentOrg();
|
||||||
|
|
||||||
|
const isAdmin = useIsOrgAdmin();
|
||||||
|
|
||||||
|
const { data, loading } = useGetOrganizationSpendingNotificationQuery({
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
variables: { orgId: org?.id },
|
||||||
|
skip: !org,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: nextInvoiceData, loading: loadingInvoice } =
|
||||||
|
useBillingGetNextInvoiceQuery({
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
variables: {
|
||||||
|
organizationID: org?.id,
|
||||||
|
},
|
||||||
|
skip: !org,
|
||||||
|
});
|
||||||
|
|
||||||
|
const amountDue = nextInvoiceData?.billingGetNextInvoice?.AmountDue ?? null;
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateOrganizationSpendingNotificationMutation({
|
||||||
|
refetchQueries: [GetOrganizationSpendingNotificationDocument],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { threshold } = data?.organizations[0] ?? {};
|
||||||
|
|
||||||
|
const form = useForm<SpendingNotificationsFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
enabled: false,
|
||||||
|
threshold: threshold ?? 0,
|
||||||
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
context: {
|
||||||
|
planPrice: org?.plan?.price ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { watch, setValue } = form;
|
||||||
|
|
||||||
|
const currentThreshold = watch('threshold');
|
||||||
|
|
||||||
|
const handleEnabledChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { checked } = event.target;
|
||||||
|
setValue('enabled', checked, { shouldDirty: true });
|
||||||
|
if (!checked) {
|
||||||
|
setValue('threshold', 0, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabled = watch('enabled');
|
||||||
|
|
||||||
|
const progress = useMemo(() => {
|
||||||
|
if (!enabled || threshold <= 0 || !amountDue) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = (amountDue / threshold) * 100;
|
||||||
|
return Math.min(Math.max(percent, 0), 100);
|
||||||
|
}, [amountDue, enabled, threshold]);
|
||||||
|
|
||||||
|
const handleThresholdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.value === '') {
|
||||||
|
setValue('threshold', undefined, { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
setValue('threshold', Number(event.target.value), { shouldDirty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
form.reset({
|
||||||
|
enabled: !!threshold,
|
||||||
|
threshold,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [loading, threshold, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: SpendingNotificationsFormValues) => {
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
|
variables: {
|
||||||
|
id: org?.id,
|
||||||
|
threshold: values.threshold,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await execPromiseWithErrorToast(
|
||||||
|
async () => {
|
||||||
|
await updateConfigPromise;
|
||||||
|
form.reset({
|
||||||
|
enabled: !!values.threshold,
|
||||||
|
threshold: values.threshold,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loadingMessage: 'Spending notifications are being updated...',
|
||||||
|
successMessage:
|
||||||
|
'Spending notifications have been updated successfully.',
|
||||||
|
errorMessage:
|
||||||
|
'An error occurred while trying to update spending notifications.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationPercentageAmount = (factor: number) => {
|
||||||
|
if (!threshold || threshold <= 0) {
|
||||||
|
return '\u00A0';
|
||||||
|
}
|
||||||
|
const amount = threshold * factor;
|
||||||
|
return `$${Math.round(amount)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputMin = useMemo(
|
||||||
|
() => Math.ceil(1.1 * (amountDue ?? 0)),
|
||||||
|
[amountDue],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading || loadingInvoice) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<div className="flex h-32 place-content-center">
|
||||||
|
<ActivityIndicator
|
||||||
|
label="Loading spending notifications..."
|
||||||
|
className="justify-center text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4 p-4"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-row items-end justify-between gap-8">
|
||||||
|
<span className="font-medium">Spending Notifications</span>
|
||||||
|
<Switch
|
||||||
|
className="self-end"
|
||||||
|
id="enabled"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={handleEnabledChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col justify-between gap-8 md:flex-row">
|
||||||
|
<div className="flex basis-1/2 flex-col gap-2">
|
||||||
|
<p className="max-w-prose">
|
||||||
|
Specify a spending threshold to receive email notifications when
|
||||||
|
your usage approaches the designated amount.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
|
{enabled && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="threshold"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-1 flex-col">
|
||||||
|
<FormLabel className="flex flex-1 flex-row items-center gap-2">
|
||||||
|
<span>Amount</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Input
|
||||||
|
prefix="$"
|
||||||
|
type="number"
|
||||||
|
min={inputMin}
|
||||||
|
placeholder="0"
|
||||||
|
disabled={!enabled}
|
||||||
|
{...field}
|
||||||
|
onChange={handleThresholdChange}
|
||||||
|
value={currentThreshold}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<Input
|
||||||
|
prefix="$"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
disabled
|
||||||
|
{...field}
|
||||||
|
onChange={handleThresholdChange}
|
||||||
|
value={currentThreshold}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Only an organization admin can change this value.
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<div className="basis-3/4" />
|
||||||
|
<div className="flex flex-1 justify-between gap-2">
|
||||||
|
<div className="flex basis-2/3 text-muted-foreground">
|
||||||
|
<span className="w-13 text-center">75%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex basis-1/3 text-muted-foreground">
|
||||||
|
<span className="w-13 text-center">90%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex basis-1/3 text-muted-foreground">
|
||||||
|
<span className="w-13 text-center">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Progress value={progress} className="h-3" />
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<div className="basis-3/4" />
|
||||||
|
<div className="flex flex-1 justify-between gap-2">
|
||||||
|
<div className="flex basis-2/3 text-muted-foreground">
|
||||||
|
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||||
|
{getNotificationPercentageAmount(0.75) || '\u00A0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex basis-1/3 text-muted-foreground">
|
||||||
|
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||||
|
{getNotificationPercentageAmount(0.9) || '\u00A0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex basis-1/3 text-muted-foreground">
|
||||||
|
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||||
|
{getNotificationPercentageAmount(1) || '\u00A0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="max-w-prose">
|
||||||
|
You'll receive email alerts when your usage reaches 75%,
|
||||||
|
90%, and 100% of your configured value. These are
|
||||||
|
notifications only - your service will continue running
|
||||||
|
normally.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-1 flex-col justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="h-fit self-end"
|
||||||
|
disabled={!form.formState.isDirty || !isAdmin}
|
||||||
|
>
|
||||||
|
{form.formState.isSubmitting ? (
|
||||||
|
<ActivityIndicator className="text-sm" />
|
||||||
|
) : (
|
||||||
|
'Save'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as SpendingNotifications } from './SpendingNotifications';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as BillingEstimate } from './BillingEstimate';
|
||||||
@@ -41,8 +41,8 @@ const changeOrgPlanForm = z.object({
|
|||||||
|
|
||||||
export default function SubscriptionPlan() {
|
export default function SubscriptionPlan() {
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { org, refetch: refetchOrg } = useCurrentOrg();
|
const { org, refetch: refetchOrg } = useCurrentOrg();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const [changeOrgPlan] = useBillingChangeOrganizationPlanMutation();
|
const [changeOrgPlan] = useBillingChangeOrganizationPlanMutation();
|
||||||
const { data: { plans = [] } = {} } = useGetOrganizationPlansQuery();
|
const { data: { plans = [] } = {} } = useGetOrganizationPlansQuery();
|
||||||
const [fetchOrganizationCustomePortalLink, { loading }] =
|
const [fetchOrganizationCustomePortalLink, { loading }] =
|
||||||
@@ -102,7 +102,6 @@ export default function SubscriptionPlan() {
|
|||||||
|
|
||||||
if (billingOrganizationCustomePortal) {
|
if (billingOrganizationCustomePortal) {
|
||||||
const newWindow = window.open(billingOrganizationCustomePortal);
|
const newWindow = window.open(billingOrganizationCustomePortal);
|
||||||
|
|
||||||
if (!newWindow) {
|
if (!newWindow) {
|
||||||
window.location.href = billingOrganizationCustomePortal;
|
window.location.href = billingOrganizationCustomePortal;
|
||||||
}
|
}
|
||||||
@@ -126,30 +125,34 @@ export default function SubscriptionPlan() {
|
|||||||
<div className="flex w-full flex-col gap-1 border-b p-4">
|
<div className="flex w-full flex-col gap-1 border-b p-4">
|
||||||
<h4 className="font-medium">Subscription plan</h4>
|
<h4 className="font-medium">Subscription plan</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col border-b md:flex-row">
|
<div className="flex w-full flex-col justify-between gap-8 border-b p-4 md:flex-row">
|
||||||
<div className="flex w-full flex-col gap-4 p-4">
|
<div className="flex basis-1/2 flex-col gap-4">
|
||||||
<span className="font-medium">Organization name</span>
|
<span className="font-medium">Organization name</span>
|
||||||
<span className="font-medium">{org?.name}</span>
|
<span className="font-medium">{org?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<div className="flex flex-1 flex-col gap-8 md:flex-row">
|
||||||
<span className="font-medium">Current plan</span>
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
<span className="text-xl font-bold text-primary">
|
<span className="font-medium">Current plan</span>
|
||||||
{org?.plan?.name}
|
<span className="text-xl font-bold text-primary">
|
||||||
</span>
|
{org?.plan?.name}
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col items-start justify-start gap-4 p-4 md:items-end md:justify-end">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xl font-semibold">
|
|
||||||
${org?.plan?.price}
|
|
||||||
</span>
|
</span>
|
||||||
<Slash
|
</div>
|
||||||
className="h-5 w-5 text-muted-foreground/40"
|
|
||||||
strokeWidth={2.5}
|
<div className="flex flex-1 items-start justify-start md:items-end md:justify-end">
|
||||||
/>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xl font-semibold">month</span>
|
<span className="text-xl font-semibold">
|
||||||
|
${org?.plan?.price}
|
||||||
|
</span>
|
||||||
|
<Slash
|
||||||
|
className="h-5 w-5 text-muted-foreground/40"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
<span className="text-xl font-semibold">month</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 p-4 md:flex-row md:items-center md:gap-0">
|
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 p-4 md:flex-row md:items-center md:gap-0">
|
||||||
<div>
|
<div>
|
||||||
<span>For a complete list of features, visit our </span>
|
<span>For a complete list of features, visit our </span>
|
||||||
@@ -164,14 +167,18 @@ export default function SubscriptionPlan() {
|
|||||||
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
|
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-end gap-2">
|
<div className="flex w-full flex-row items-center justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
className="h-fit"
|
className="h-fit truncate"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleUpdatePaymentDetails}
|
onClick={handleUpdatePaymentDetails}
|
||||||
disabled={org?.plan?.isFree || maintenanceActive || loading}
|
disabled={org?.plan?.isFree || maintenanceActive || loading}
|
||||||
>
|
>
|
||||||
{loading ? <ActivityIndicator /> : 'Stripe Customer Portal'}
|
{loading ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : (
|
||||||
|
<span className="truncate">Stripe Customer Portal</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={org?.plan?.isFree || maintenanceActive}
|
disabled={org?.plan?.isFree || maintenanceActive}
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
|
||||||
import { Progress } from '@/components/ui/v3/progress';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@/components/ui/v3/table';
|
|
||||||
import { getBillingCycleInfo } from '@/features/orgs/components/billing/utils/getBillingCycle';
|
|
||||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
|
||||||
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
|
||||||
|
|
||||||
export default function Usage() {
|
|
||||||
const { org } = useCurrentOrg();
|
|
||||||
const { billingCycleRange, progress } = getBillingCycleInfo();
|
|
||||||
const { data, loading } = useBillingGetNextInvoiceQuery({
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
variables: {
|
|
||||||
organizationID: org?.id,
|
|
||||||
},
|
|
||||||
skip: !org,
|
|
||||||
});
|
|
||||||
|
|
||||||
const billingItems = data?.billingGetNextInvoice?.items ?? [];
|
|
||||||
const amountDue = data?.billingGetNextInvoice?.AmountDue ?? null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="font-medium">
|
|
||||||
<div className="flex flex-col w-full border rounded-md bg-background">
|
|
||||||
<div className="flex flex-col w-full gap-1 p-4">
|
|
||||||
<span>Usage</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex flex-row items-center justify-between w-full p-4 border-t border-b">
|
|
||||||
<span>Billing cycle ({billingCycleRange})</span>
|
|
||||||
<Progress value={progress} className="h-2 max-w-xl" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 p-4">
|
|
||||||
{loading && (
|
|
||||||
<div className="flex h-32 place-content-center">
|
|
||||||
<ActivityIndicator
|
|
||||||
label="Loading usage stats..."
|
|
||||||
className="justify-center text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && data && (
|
|
||||||
<>
|
|
||||||
<span>Breakdown</span>
|
|
||||||
<div className="border rounded-md">
|
|
||||||
<Table>
|
|
||||||
<TableHeader className="w-full bg-accent">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
|
||||||
Item
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right rounded-tr-md">
|
|
||||||
Amount
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{billingItems.map((billingItem) => (
|
|
||||||
<TableRow key={billingItem.Description}>
|
|
||||||
<TableCell colSpan={3}>
|
|
||||||
{billingItem.Description}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell colSpan={3} className="text-right">
|
|
||||||
${billingItem.Amount}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
<TableFooter className="bg-accent">
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={3} className="rounded-bl-md">
|
|
||||||
Total
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right rounded-br-md">
|
|
||||||
${amountDue}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableFooter>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as Usage } from './Usage';
|
|
||||||
@@ -18,9 +18,13 @@ export const getBillingCycleInfo = () => {
|
|||||||
(now.getTime() - startOfMonth.getTime()) / (1000 * 60 * 60 * 24) + 1;
|
(now.getTime() - startOfMonth.getTime()) / (1000 * 60 * 60 * 24) + 1;
|
||||||
|
|
||||||
const progress = (daysPassed / totalDays) * 100;
|
const progress = (daysPassed / totalDays) * 100;
|
||||||
|
const daysLeft = Math.max(Math.ceil(totalDays - daysPassed), 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
billingCycleStart,
|
||||||
|
billingCycleEnd,
|
||||||
billingCycleRange: `${billingCycleStart} - ${billingCycleEnd}`,
|
billingCycleRange: `${billingCycleStart} - ${billingCycleEnd}`,
|
||||||
progress: Math.min(Math.max(progress, 0), 100), // Ensure the value is between 0 and 100
|
progress: Math.min(Math.max(progress, 0), 100), // Ensure the value is between 0 and 100
|
||||||
|
daysLeft,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
|
import { Badge } from '@/components/ui/v3/badge';
|
||||||
import { Button } from '@/components/ui/v3/button';
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/v3/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -7,15 +14,64 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/components/ui/v3/sheet';
|
} from '@/components/ui/v3/sheet';
|
||||||
import { Announcements } from '@/features/projects/common/components/Announcements';
|
import {
|
||||||
import { Megaphone } from 'lucide-react';
|
useDeleteAnnouncementReadMutation,
|
||||||
|
useGetAnnouncementsQuery,
|
||||||
|
useInsertAnnouncementReadMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { useAuthenticationStatus } from '@nhost/nextjs';
|
||||||
|
import { formatDistance } from 'date-fns';
|
||||||
|
import { EllipsisVertical, Megaphone } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function AnnouncementsTray() {
|
export default function AnnouncementsTray() {
|
||||||
|
const { isAuthenticated } = useAuthenticationStatus();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
refetch: refetchAnnouncements,
|
||||||
|
} = useGetAnnouncementsQuery({
|
||||||
|
skip: !isAuthenticated,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [insertAnnouncementRead] = useInsertAnnouncementReadMutation();
|
||||||
|
const [deleteAnnouncementRead] = useDeleteAnnouncementReadMutation();
|
||||||
|
|
||||||
|
const announcements = data?.announcements ?? [];
|
||||||
|
const unreadAnnouncementsCount = announcements.filter(
|
||||||
|
(ann) => ann.read.length === 0,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const handleSetUnread = async (announcementReadId: string) => {
|
||||||
|
await deleteAnnouncementRead({
|
||||||
|
variables: {
|
||||||
|
id: announcementReadId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetchAnnouncements();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetRead = async (announcementID: string) => {
|
||||||
|
await insertAnnouncementRead({
|
||||||
|
variables: { announcementID },
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetchAnnouncements();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" className="relative h-fit px-3 py-1">
|
<Button
|
||||||
<Megaphone className="h-5 w-5" />
|
variant="outline"
|
||||||
|
className="relative flex h-8 items-center gap-2 px-2"
|
||||||
|
>
|
||||||
|
<Megaphone className="h-4.5 w-4.5" />
|
||||||
|
{unreadAnnouncementsCount > 0 && (
|
||||||
|
<Badge variant="destructive">{unreadAnnouncementsCount}</Badge>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="h-full w-full bg-background p-0 text-foreground sm:max-w-[310px]">
|
<SheetContent className="h-full w-full bg-background p-0 text-foreground sm:max-w-[310px]">
|
||||||
@@ -25,8 +81,102 @@ export default function AnnouncementsTray() {
|
|||||||
Latest news and announcements.
|
Latest news and announcements.
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex h-full w-full flex-col px-8 pt-3">
|
<div className="flex h-full w-full flex-col">
|
||||||
<Announcements />
|
<div className="flex h-12 items-center border-b px-2">
|
||||||
|
<h3 className="font-medium">
|
||||||
|
Latest Announcements{' '}
|
||||||
|
{unreadAnnouncementsCount > 0 && `(${unreadAnnouncementsCount})`}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-full flex-col gap-2 overflow-auto p-2">
|
||||||
|
{!loading && announcements.length === 0 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
No new announcements
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{announcements.map((announcement) => (
|
||||||
|
<Button
|
||||||
|
key={announcement.id}
|
||||||
|
variant="ghost"
|
||||||
|
asChild
|
||||||
|
className="h-fit w-full items-start gap-2 rounded-md border p-2"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={announcement.href}
|
||||||
|
shallow
|
||||||
|
onClick={() => {
|
||||||
|
if (announcement.read.length === 0) {
|
||||||
|
handleSetRead(announcement.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{announcement.read.length === 0 ? (
|
||||||
|
<span className="mt-[5px] h-2 w-2 flex-shrink-0 rounded-full bg-primary" />
|
||||||
|
) : (
|
||||||
|
<span className="mt-[5px] h-2 w-2 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatDistance(
|
||||||
|
new Date(announcement.createdAt),
|
||||||
|
new Date(),
|
||||||
|
{
|
||||||
|
addSuffix: true,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<p className="whitespace-normal">
|
||||||
|
{announcement.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-3"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute">
|
||||||
|
<EllipsisVertical className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
sideOffset={-5}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={announcement.read.length > 0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSetRead(announcement.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mark as read
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={announcement.read.length === 0}
|
||||||
|
onClick={() =>
|
||||||
|
handleSetUnread(announcement.read.at(0).id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Mark as unread
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
useOrganizationMemberInvitesLazyQuery,
|
useOrganizationMemberInvitesLazyQuery,
|
||||||
useOrganizationNewRequestsLazyQuery,
|
useOrganizationNewRequestsLazyQuery,
|
||||||
usePostOrganizationRequestMutation,
|
usePostOrganizationRequestMutation,
|
||||||
|
type OrganizationMemberInvitesQuery,
|
||||||
type PostOrganizationRequestResponse,
|
type PostOrganizationRequestResponse,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useUserData } from '@nhost/nextjs';
|
import { useUserData } from '@nhost/nextjs';
|
||||||
@@ -33,10 +34,13 @@ import { Bell } from 'lucide-react';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Invite = OrganizationMemberInvitesQuery['organizationMemberInvites'][0];
|
||||||
|
|
||||||
export default function NotificationsTray() {
|
export default function NotificationsTray() {
|
||||||
const userData = useUserData();
|
const userData = useUserData();
|
||||||
const { asPath, route } = useRouter();
|
const { asPath, route, push } = useRouter();
|
||||||
const { refetch: refetchOrgs } = useOrgs();
|
const { refetch: refetchOrgs } = useOrgs();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
|
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
|
||||||
|
|
||||||
@@ -113,17 +117,19 @@ export default function NotificationsTray() {
|
|||||||
const [acceptInvite] = useOrganizationMemberInviteAcceptMutation();
|
const [acceptInvite] = useOrganizationMemberInviteAcceptMutation();
|
||||||
const [deleteInvite] = useDeleteOrganizationMemberInviteMutation();
|
const [deleteInvite] = useDeleteOrganizationMemberInviteMutation();
|
||||||
|
|
||||||
const handleAccept = async (inviteId: string) => {
|
const handleAccept = async (invite: Invite) => {
|
||||||
await execPromiseWithErrorToast(
|
await execPromiseWithErrorToast(
|
||||||
async () => {
|
async () => {
|
||||||
await acceptInvite({
|
await acceptInvite({
|
||||||
variables: {
|
variables: {
|
||||||
inviteId,
|
inviteId: invite.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
refetchInvites();
|
await refetchInvites();
|
||||||
refetchOrgs();
|
await refetchOrgs();
|
||||||
|
await push(`/orgs/${invite?.organization?.slug}/projects`);
|
||||||
|
setOpen(false);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loadingMessage: `Accepting invite...`,
|
loadingMessage: `Accepting invite...`,
|
||||||
@@ -154,12 +160,18 @@ export default function NotificationsTray() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sheet>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" className="relative h-fit px-3 py-1">
|
<Button
|
||||||
<Bell className="mt-[2px] h-[1.15rem] w-[1.15rem]" />
|
variant="outline"
|
||||||
{(pendingOrgRequest || Boolean(invites.length)) && (
|
className="relative flex h-8 items-center gap-2 border px-2"
|
||||||
<div className="absolute right-3 top-2 h-2 w-2 rounded-full bg-red-500" />
|
aria-label="Notifications"
|
||||||
|
>
|
||||||
|
<Bell className="h-4.5 w-4.5" />
|
||||||
|
{(pendingOrgRequest || invites.length > 0) && (
|
||||||
|
<Badge variant="destructive">
|
||||||
|
{invites.length + (pendingOrgRequest ? 1 : 0)}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
@@ -248,7 +260,7 @@ export default function NotificationsTray() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="h-fit"
|
className="h-fit"
|
||||||
onClick={() => handleAccept(invite.id)}
|
onClick={() => handleAccept(invite)}
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Alert } from '@/components/ui/v2/Alert';
|
|||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Box } from '@/components/ui/v2/Box';
|
||||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||||
|
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||||
@@ -15,7 +16,7 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
|||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo, type ReactNode } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
|
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
|
||||||
@@ -34,11 +35,53 @@ function ProjectLayoutContent({
|
|||||||
query: { appSubdomain },
|
query: { appSubdomain },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
|
|
||||||
const isPlatform = useIsPlatform();
|
|
||||||
const { state } = useAppState();
|
const { state } = useAppState();
|
||||||
|
const isPlatform = useIsPlatform();
|
||||||
const { project, loading, error } = useProject({ poll: true });
|
const { project, loading, error } = useProject({ poll: true });
|
||||||
|
|
||||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||||
|
|
||||||
|
const renderPausedProjectContent = useCallback(
|
||||||
|
(_children: ReactNode) => {
|
||||||
|
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||||
|
const blockedPausedProjectPages = [
|
||||||
|
'database',
|
||||||
|
'database/browser/[dataSourceSlug]',
|
||||||
|
'graphql',
|
||||||
|
'hasura',
|
||||||
|
'users',
|
||||||
|
'storage',
|
||||||
|
'ai/auto-embeddings',
|
||||||
|
'ai/assistants',
|
||||||
|
'metrics',
|
||||||
|
].map((page) => baseProjectPageRoute.concat(page));
|
||||||
|
|
||||||
|
// show an alert box on top of the overview page with a wake up button
|
||||||
|
if (isOnOverviewPage) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||||
|
<ApplicationPausedBanner
|
||||||
|
alertClassName="flex-row"
|
||||||
|
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||||
|
wakeUpButtonClassName="w-fit self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// block these pages when the project is paused
|
||||||
|
if (blockedPausedProjectPages.includes(route)) {
|
||||||
|
return <ApplicationPaused />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _children;
|
||||||
|
},
|
||||||
|
[route, isOnOverviewPage, children],
|
||||||
|
);
|
||||||
|
|
||||||
// Render application state based on the current state
|
// Render application state based on the current state
|
||||||
const projectPageContent = useMemo(() => {
|
const projectPageContent = useMemo(() => {
|
||||||
if (!appSubdomain || state === undefined) {
|
if (!appSubdomain || state === undefined) {
|
||||||
@@ -67,7 +110,7 @@ function ProjectLayoutContent({
|
|||||||
return children;
|
return children;
|
||||||
case ApplicationStatus.Pausing:
|
case ApplicationStatus.Pausing:
|
||||||
case ApplicationStatus.Paused:
|
case ApplicationStatus.Paused:
|
||||||
return <ApplicationPaused />;
|
return renderPausedProjectContent(children);
|
||||||
case ApplicationStatus.Unpausing:
|
case ApplicationStatus.Unpausing:
|
||||||
return <ApplicationUnpausing />;
|
return <ApplicationUnpausing />;
|
||||||
case ApplicationStatus.Restoring:
|
case ApplicationStatus.Restoring:
|
||||||
@@ -79,7 +122,13 @@ function ProjectLayoutContent({
|
|||||||
default:
|
default:
|
||||||
return <ApplicationUnknown />;
|
return <ApplicationUnknown />;
|
||||||
}
|
}
|
||||||
}, [state, children, appSubdomain, isOnOverviewPage]);
|
}, [
|
||||||
|
state,
|
||||||
|
children,
|
||||||
|
appSubdomain,
|
||||||
|
isOnOverviewPage,
|
||||||
|
renderPausedProjectContent,
|
||||||
|
]);
|
||||||
|
|
||||||
// Handle loading state
|
// Handle loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="underline"
|
||||||
href="https://docs.nhost.io/cli/overlays"
|
href="https://docs.nhost.io/guides/cli/configuration-overlays#configuration-overlays"
|
||||||
>
|
>
|
||||||
docs.nhost.io/cli/overlays
|
Configuration Overlays
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for guidance.
|
for guidance.
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
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 { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||||
@@ -26,11 +26,12 @@ export default function DisableAIServiceConfirmationDialog({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onServiceDisabled,
|
onServiceDisabled,
|
||||||
}: DisableAIServiceConfirmationDialogProps) {
|
}: DisableAIServiceConfirmationDialogProps) {
|
||||||
|
const { project } = useProject();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { openDialog, closeDialog } = useDialog();
|
const { openDialog, closeDialog } = useDialog();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
@@ -42,7 +43,7 @@ export default function DisableAIServiceConfirmationDialog({
|
|||||||
async () => {
|
async () => {
|
||||||
await updateConfig({
|
await updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
appId: currentProject.id,
|
appId: project?.id,
|
||||||
config: {
|
config: {
|
||||||
ai: null,
|
ai: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||||
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
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 { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
|
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||||
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
import {
|
||||||
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OTPEmailSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
|
export default function OTPEmailSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
|
const isPlatform = useIsPlatform();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
|
variables: { appId: project?.id },
|
||||||
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { enabled } = data?.config?.auth?.method?.otp?.email || {};
|
||||||
|
|
||||||
|
const form = useForm<OTPEmailSettingsFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
enabled,
|
||||||
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
form.reset({ enabled });
|
||||||
|
}
|
||||||
|
}, [loading, enabled, form]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading one-time passwords over email settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOTPEmailSettingsChange = async (
|
||||||
|
values: OTPEmailSettingsFormValues,
|
||||||
|
) => {
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
|
variables: {
|
||||||
|
appId: project.id,
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
method: {
|
||||||
|
otp: {
|
||||||
|
email: {
|
||||||
|
enabled: values.enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await execPromiseWithErrorToast(
|
||||||
|
async () => {
|
||||||
|
await updateConfigPromise;
|
||||||
|
form.reset(values);
|
||||||
|
|
||||||
|
if (!isPlatform) {
|
||||||
|
openDialog({
|
||||||
|
title: 'Apply your changes',
|
||||||
|
component: <ApplyLocalSettingsDialog />,
|
||||||
|
props: {
|
||||||
|
PaperProps: {
|
||||||
|
className: 'max-w-2xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loadingMessage:
|
||||||
|
'One-time passwords over email settings are being updated...',
|
||||||
|
successMessage:
|
||||||
|
'One-time passwords over email settings have been updated successfully.',
|
||||||
|
errorMessage:
|
||||||
|
'An error occurred while trying to update one-time passwords over email settings.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleOTPEmailSettingsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="One-Time Passwords over email"
|
||||||
|
description="Allow users to sign in with a one-time password sent to their email address."
|
||||||
|
slotProps={{
|
||||||
|
submitButton: {
|
||||||
|
disabled: !form.formState.isDirty || maintenanceActive,
|
||||||
|
loading: form.formState.isSubmitting,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
switchId="enabled"
|
||||||
|
showSwitch
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as OTPEmailSettings } from './OTPEmailSettings';
|
||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -32,13 +31,13 @@ export type AllowedEmailSettingsFormValues = Yup.InferType<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export default function AllowedEmailDomainsSettings() {
|
export default function AllowedEmailDomainsSettings() {
|
||||||
const { openDialog } = useDialog();
|
const { project } = useProject();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -29,13 +28,13 @@ export type AllowedRedirectURLFormValues = Yup.InferType<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export default function AllowedRedirectURLsSettings() {
|
export default function AllowedRedirectURLsSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const { project } = useProject();
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
|||||||
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function AnonymousSignInSettings() {
|
export default function AnonymousSignInSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -51,6 +50,7 @@ const validationSchema = Yup.object({
|
|||||||
is: true,
|
is: true,
|
||||||
then: (schema) => schema.required(),
|
then: (schema) => schema.required(),
|
||||||
}),
|
}),
|
||||||
|
audience: Yup.string().label('Audience'),
|
||||||
enabled: Yup.boolean(),
|
enabled: Yup.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
|
|||||||
|
|
||||||
export default function AppleProviderSettings() {
|
export default function AppleProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export default function AppleProviderSettings() {
|
|||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { clientId, enabled, keyId, privateKey, teamId } =
|
const { clientId, enabled, keyId, privateKey, teamId, audience } =
|
||||||
data?.config?.auth?.method?.oauth?.apple || {};
|
data?.config?.auth?.method?.oauth?.apple || {};
|
||||||
|
|
||||||
const form = useForm<AppleProviderFormValues>({
|
const form = useForm<AppleProviderFormValues>({
|
||||||
@@ -83,6 +83,7 @@ export default function AppleProviderSettings() {
|
|||||||
keyId: keyId || '',
|
keyId: keyId || '',
|
||||||
clientId: clientId || '',
|
clientId: clientId || '',
|
||||||
privateKey: privateKey || '',
|
privateKey: privateKey || '',
|
||||||
|
audience: audience || '',
|
||||||
enabled: enabled || false,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
@@ -95,10 +96,11 @@ export default function AppleProviderSettings() {
|
|||||||
keyId: keyId || '',
|
keyId: keyId || '',
|
||||||
clientId: clientId || '',
|
clientId: clientId || '',
|
||||||
privateKey: privateKey || '',
|
privateKey: privateKey || '',
|
||||||
|
audience: audience || '',
|
||||||
enabled: enabled || false,
|
enabled: enabled || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loading, teamId, keyId, clientId, privateKey, enabled, form]);
|
}, [loading, teamId, keyId, clientId, privateKey, audience, enabled, form]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -238,6 +240,18 @@ export default function AppleProviderSettings() {
|
|||||||
error={!!formState.errors?.privateKey}
|
error={!!formState.errors?.privateKey}
|
||||||
helperText={formState.errors?.privateKey?.message}
|
helperText={formState.errors?.privateKey?.message}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('audience')}
|
||||||
|
name="audience"
|
||||||
|
id="audience"
|
||||||
|
label="Audience (optional)"
|
||||||
|
placeholder="Apple Audience"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.audience}
|
||||||
|
helperText={formState.errors?.audience?.message}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="apple-redirectUrl"
|
id="apple-redirectUrl"
|
||||||
@@ -269,7 +283,7 @@ export default function AppleProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
Software_Type_Enum,
|
Software_Type_Enum,
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useGetSoftwareVersionsQuery,
|
useGetSoftwareVersionsQuery,
|
||||||
@@ -34,13 +33,13 @@ export type AuthServiceVersionFormValues = Yup.InferType<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export default function AuthServiceVersionSettings() {
|
export default function AuthServiceVersionSettings() {
|
||||||
const { openDialog } = useDialog();
|
|
||||||
const isPlatform = useIsPlatform();
|
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
|
||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
|
const isPlatform = useIsPlatform();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,7 +153,7 @@ export default function AuthServiceVersionSettings() {
|
|||||||
}}
|
}}
|
||||||
docsLink="https://github.com/nhost/hasura-auth/releases"
|
docsLink="https://github.com/nhost/hasura-auth/releases"
|
||||||
docsTitle="the latest releases"
|
docsTitle="the latest releases"
|
||||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||||
>
|
>
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
id="version"
|
id="version"
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -51,13 +50,13 @@ const validationSchema = Yup.object({
|
|||||||
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
|
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function AzureADProviderSettings() {
|
export default function AzureADProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const { project } = useProject();
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,7 +213,7 @@ export default function AzureADProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -30,13 +29,13 @@ const validationSchema = Yup.object({
|
|||||||
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function BlockedEmailSettings() {
|
export default function BlockedEmailSettings() {
|
||||||
const { openDialog } = useDialog();
|
const { project } = useProject();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -27,13 +26,13 @@ const validationSchema = Yup.object({
|
|||||||
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function ClientURLSettings() {
|
export default function ClientURLSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
@@ -27,13 +26,13 @@ export type ToggleConcealErrorsFormValues = Yup.InferType<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export default function ConcealErrorsSettings() {
|
export default function ConcealErrorsSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSmtpSettingsDocument,
|
useGetSmtpSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -52,16 +52,24 @@ function ConfirmDeleteSMTPSettingsModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeleteSMTPSettings() {
|
export default function DeleteSMTPSettings() {
|
||||||
const { openDialog, closeDialog } = useDialog();
|
const { project } = useProject();
|
||||||
|
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { project } = useProject();
|
const { openDialog, closeDialog } = useDialog();
|
||||||
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
|
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||||
|
variables: { appId: project?.id },
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const smtpSettings = data?.config?.provider?.smtp ?? {};
|
||||||
|
|
||||||
|
const isSMTPConfigured = Boolean(Object.keys(smtpSettings).length);
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSmtpSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,7 +119,10 @@ export default function DeleteSMTPSettings() {
|
|||||||
component: (
|
component: (
|
||||||
<ConfirmDeleteSMTPSettingsModal
|
<ConfirmDeleteSMTPSettingsModal
|
||||||
close={closeDialog}
|
close={closeDialog}
|
||||||
onDelete={deleteSMTPSettings}
|
onDelete={async () => {
|
||||||
|
await deleteSMTPSettings();
|
||||||
|
await refetch();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -132,7 +143,7 @@ export default function DeleteSMTPSettings() {
|
|||||||
color="error"
|
color="error"
|
||||||
className="mx-4 mt-4 justify-self-end"
|
className="mx-4 mt-4 justify-self-end"
|
||||||
onClick={confirmDeleteSMTPSettings}
|
onClick={confirmDeleteSMTPSettings}
|
||||||
disabled={loading || maintenanceActive}
|
disabled={loading || maintenanceActive || !isSMTPConfigured}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
|||||||
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function DisableNewUsersSettings() {
|
export default function DisableNewUsersSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,14 +42,14 @@ export default function DisableNewUsersSettings() {
|
|||||||
const form = useForm<DisableNewUsersFormValues>({
|
const form = useForm<DisableNewUsersFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
form.reset({
|
form.reset({
|
||||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loading, data, form]);
|
}, [loading, data, form]);
|
||||||
@@ -59,7 +58,7 @@ export default function DisableNewUsersSettings() {
|
|||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading disabled sign up settings..."
|
label="Loading disabled new users settings..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -80,7 +79,7 @@ export default function DisableNewUsersSettings() {
|
|||||||
config: {
|
config: {
|
||||||
auth: {
|
auth: {
|
||||||
signUp: {
|
signUp: {
|
||||||
enabled: !values.disabled,
|
disableNewUsers: values.disabled,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -105,10 +104,10 @@ export default function DisableNewUsersSettings() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loadingMessage: 'Disabling new user sign ups...',
|
loadingMessage: 'Disabling new users sign ins...',
|
||||||
successMessage: 'New user sign ups have been disabled successfully.',
|
successMessage: 'New users sign ins have been disabled successfully.',
|
||||||
errorMessage:
|
errorMessage:
|
||||||
'An error occurred while trying to disable new user sign ups.',
|
'An error occurred while trying to disable new users sign ins.',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||||
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
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 {
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
|
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||||
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
disabled: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DisableSignUpsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
|
export default function DisableSignUpsSettings() {
|
||||||
|
const { openDialog } = useDialog();
|
||||||
|
const isPlatform = useIsPlatform();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
const { project } = useProject();
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: project?.id },
|
||||||
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<DisableSignUpsFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
form.reset({
|
||||||
|
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [loading, data, form]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading disabled sign up settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formState } = form;
|
||||||
|
|
||||||
|
const handleDisableSignUpsChange = async (
|
||||||
|
values: DisableSignUpsFormValues,
|
||||||
|
) => {
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
|
variables: {
|
||||||
|
appId: project.id,
|
||||||
|
config: {
|
||||||
|
auth: {
|
||||||
|
signUp: {
|
||||||
|
enabled: !values.disabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await execPromiseWithErrorToast(
|
||||||
|
async () => {
|
||||||
|
await updateConfigPromise;
|
||||||
|
form.reset(values);
|
||||||
|
|
||||||
|
if (!isPlatform) {
|
||||||
|
openDialog({
|
||||||
|
title: 'Apply your changes',
|
||||||
|
component: <ApplyLocalSettingsDialog />,
|
||||||
|
props: {
|
||||||
|
PaperProps: {
|
||||||
|
className: 'max-w-2xl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loadingMessage: 'Disabling new users sign ups...',
|
||||||
|
successMessage: 'New users sign ups have been disabled successfully.',
|
||||||
|
errorMessage:
|
||||||
|
'An error occurred while trying to disable new users sign ups.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleDisableSignUpsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Disable Sign Ups"
|
||||||
|
description="If set, new users won't be able to sign up."
|
||||||
|
docsLink="https://docs.nhost.io/guides/auth/overview#disable-sign-ups"
|
||||||
|
switchId="disabled"
|
||||||
|
showSwitch
|
||||||
|
slotProps={{
|
||||||
|
submitButton: {
|
||||||
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as DisableSignUpsSettings } from './DisableSignUpsSettings';
|
||||||
@@ -19,7 +19,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -30,13 +29,13 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function DiscordProviderSettings() {
|
export default function DiscordProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,7 +183,7 @@ export default function DiscordProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
|||||||
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 {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
|||||||
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function EmailAndPasswordSettings() {
|
export default function EmailAndPasswordSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
|||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
|
||||||
export default function FacebookProviderSettings() {
|
export default function FacebookProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ export default function FacebookProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
|||||||
|
|
||||||
export default function GitHubProviderSettings() {
|
export default function GitHubProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,7 +190,7 @@ export default function GitHubProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ import { Input } from '@/components/ui/v2/Input';
|
|||||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
BaseProviderSettings,
|
|
||||||
baseProviderValidationSchema,
|
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
|
||||||
import {
|
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -29,15 +24,37 @@ import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/gen
|
|||||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
const googleProviderValidationSchema = Yup.object({
|
||||||
|
clientId: Yup.string()
|
||||||
|
.label('Client ID')
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (schema) => schema.required(),
|
||||||
|
}),
|
||||||
|
clientSecret: Yup.string()
|
||||||
|
.label('Client Secret')
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (schema) => schema.required(),
|
||||||
|
}),
|
||||||
|
audience: Yup.string().label('Audience'),
|
||||||
|
enabled: Yup.bool(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GoogleProviderFormValues = Yup.InferType<
|
||||||
|
typeof googleProviderValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function GoogleProviderSettings() {
|
export default function GoogleProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,17 +63,18 @@ export default function GoogleProviderSettings() {
|
|||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { clientId, clientSecret, enabled } =
|
const { clientId, clientSecret, enabled, audience } =
|
||||||
data?.config?.auth?.method?.oauth?.google || {};
|
data?.config?.auth?.method?.oauth?.google || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<GoogleProviderFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
clientId: clientId || '',
|
clientId: clientId || '',
|
||||||
clientSecret: clientSecret || '',
|
clientSecret: clientSecret || '',
|
||||||
|
audience: audience || '',
|
||||||
enabled: enabled || false,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
resolver: yupResolver(baseProviderValidationSchema),
|
resolver: yupResolver(googleProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,10 +82,11 @@ export default function GoogleProviderSettings() {
|
|||||||
form.reset({
|
form.reset({
|
||||||
clientId: clientId || '',
|
clientId: clientId || '',
|
||||||
clientSecret: clientSecret || '',
|
clientSecret: clientSecret || '',
|
||||||
|
audience: audience || '',
|
||||||
enabled: enabled || false,
|
enabled: enabled || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [loading, clientId, clientSecret, enabled, form]);
|
}, [loading, clientId, clientSecret, audience, enabled, form]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +102,7 @@ export default function GoogleProviderSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch, register } = form;
|
||||||
const authEnabled = watch('enabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||||
@@ -149,11 +168,44 @@ export default function GoogleProviderSettings() {
|
|||||||
switchId="enabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-3 gap-x-3 gap-y-4 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings providerName="google" />
|
<Input
|
||||||
|
{...register('clientId')}
|
||||||
|
id="google-clientId"
|
||||||
|
label="Client ID"
|
||||||
|
placeholder="Enter your Client ID"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.clientId}
|
||||||
|
helperText={formState.errors?.clientId?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('clientSecret')}
|
||||||
|
id="google-clientSecret"
|
||||||
|
label="Client Secret"
|
||||||
|
placeholder="Enter your Client Secret"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.clientSecret}
|
||||||
|
helperText={formState.errors?.clientSecret?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('audience')}
|
||||||
|
name="audience"
|
||||||
|
id="audience"
|
||||||
|
label="Audience (optional)"
|
||||||
|
placeholder="Enter Audience"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.audience}
|
||||||
|
helperText={formState.errors?.audience?.message}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="google-redirectUrl"
|
id="google-redirectUrl"
|
||||||
@@ -185,7 +237,7 @@ export default function GoogleProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Option } from '@/components/ui/v2/Option';
|
import { Option } from '@/components/ui/v2/Option';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
|||||||
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function GravatarSettings() {
|
export default function GravatarSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
|||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
|
||||||
export default function LinkedInProviderSettings() {
|
export default function LinkedInProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ export default function LinkedInProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -29,18 +28,19 @@ const validationSchema = Yup.object({
|
|||||||
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function MFASettings() {
|
export default function MFASettings() {
|
||||||
const { openDialog } = useDialog();
|
const { project } = useProject();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
variables: { appId: project?.id },
|
variables: { appId: project?.id },
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
|||||||
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
|
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function MagicLinkSettings() {
|
export default function MagicLinkSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetSmtpSettingsDocument,
|
|
||||||
useGetSmtpSettingsQuery,
|
useGetSmtpSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
@@ -28,21 +27,21 @@ const validationSchema = yup
|
|||||||
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
|
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function PostmarkSettings() {
|
export default function PostmarkSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
|
|
||||||
const { data } = useGetSmtpSettingsQuery({
|
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||||
variables: { appId: project?.id },
|
variables: { appId: project?.id },
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sender, password } = data?.config?.provider?.smtp || {};
|
const { sender, password } = data?.config?.provider?.smtp || {};
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSmtpSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,6 +80,8 @@ export default function PostmarkSettings() {
|
|||||||
await execPromiseWithErrorToast(
|
await execPromiseWithErrorToast(
|
||||||
async () => {
|
async () => {
|
||||||
await updateConfigPromise;
|
await updateConfigPromise;
|
||||||
|
form.reset({ ...values });
|
||||||
|
await refetch();
|
||||||
|
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
openDialog({
|
openDialog({
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Option } from '@/components/ui/v2/Option';
|
|||||||
import { Select } from '@/components/ui/v2/Select';
|
import { Select } from '@/components/ui/v2/Select';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -50,17 +49,17 @@ const validationSchema = Yup.object({
|
|||||||
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function SMSSettings() {
|
export default function SMSSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, error, loading } = useGetSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: { appId: project?.id },
|
variables: { appId: project?.id },
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetSmtpSettingsDocument,
|
|
||||||
useGetSmtpSettingsQuery,
|
useGetSmtpSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
@@ -38,14 +37,15 @@ const smtpValidationSchema = yup
|
|||||||
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||||
|
|
||||||
export default function SMTPSettings() {
|
export default function SMTPSettings() {
|
||||||
const { maintenanceActive } = useUI();
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
|
|
||||||
const { data } = useGetSmtpSettingsQuery({
|
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||||
variables: { appId: project?.id },
|
variables: { appId: project?.id },
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,6 @@ export default function SMTPSettings() {
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSmtpSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,6 +103,8 @@ export default function SMTPSettings() {
|
|||||||
await execPromiseWithErrorToast(
|
await execPromiseWithErrorToast(
|
||||||
async () => {
|
async () => {
|
||||||
await updateConfigPromise;
|
await updateConfigPromise;
|
||||||
|
form.reset({ ...values });
|
||||||
|
await refetch();
|
||||||
|
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
openDialog({
|
openDialog({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
|||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import {
|
import {
|
||||||
GetAuthenticationSettingsDocument,
|
|
||||||
useGetAuthenticationSettingsQuery,
|
useGetAuthenticationSettingsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -34,13 +33,13 @@ const validationSchema = Yup.object({
|
|||||||
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
|
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function SessionSettings() {
|
export default function SessionSettings() {
|
||||||
const { openDialog } = useDialog();
|
const { project } = useProject();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
|||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
|
||||||
export default function SpotifyProviderSettings() {
|
export default function SpotifyProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,7 +184,7 @@ export default function SpotifyProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
|||||||
|
|
||||||
export default function TwitchProviderSettings() {
|
export default function TwitchProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const { project } = useProject();
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,7 +190,7 @@ export default function TwitchProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
|||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -45,13 +44,13 @@ const validationSchema = Yup.object({
|
|||||||
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
|
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function TwitterProviderSettings() {
|
export default function TwitterProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@ export default function TwitterProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
|||||||
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
|
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function WebAuthnSettings() {
|
export default function WebAuthnSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const localMimirClient = useLocalMimirClient();
|
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const { project } = useProject();
|
const localMimirClient = useLocalMimirClient();
|
||||||
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
baseProviderValidationSchema,
|
baseProviderValidationSchema,
|
||||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
|||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
|
||||||
export default function WindowsLiveProviderSettings() {
|
export default function WindowsLiveProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,7 +182,7 @@ export default function WindowsLiveProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import {
|
import {
|
||||||
GetSignInMethodsDocument,
|
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateConfigMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
@@ -57,13 +56,13 @@ const validationSchema = Yup.object({
|
|||||||
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
|
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function WorkOsProviderSettings() {
|
export default function WorkOsProviderSettings() {
|
||||||
|
const { project } = useProject();
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { maintenanceActive } = useUI();
|
const { maintenanceActive } = useUI();
|
||||||
const localMimirClient = useLocalMimirClient();
|
const localMimirClient = useLocalMimirClient();
|
||||||
const { project } = useProject();
|
|
||||||
const [updateConfig] = useUpdateConfigMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSignInMethodsDocument],
|
|
||||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +243,7 @@ export default function WorkOsProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ query GetAuthenticationSettings($appId: uuid!) {
|
|||||||
}
|
}
|
||||||
signUp {
|
signUp {
|
||||||
enabled
|
enabled
|
||||||
|
disableNewUsers
|
||||||
}
|
}
|
||||||
session {
|
session {
|
||||||
accessToken {
|
accessToken {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { Form } from '@/components/form/Form';
|
|||||||
import { Alert } from '@/components/ui/v2/Alert';
|
import { Alert } from '@/components/ui/v2/Alert';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
import { Button } from '@/components/ui/v2/Button';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import type { DialogFormProps } from '@/types/common';
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
|
||||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||||
import {
|
import {
|
||||||
useGetSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
@@ -38,10 +38,10 @@ export default function EditUserPasswordForm({
|
|||||||
client: remoteProjectGQLClient,
|
client: remoteProjectGQLClient,
|
||||||
});
|
});
|
||||||
const { closeDialog } = useDialog();
|
const { closeDialog } = useDialog();
|
||||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
const { project } = useProject();
|
||||||
const { data } = useGetSignInMethodsQuery({
|
const { data } = useGetSignInMethodsQuery({
|
||||||
variables: { appId: currentProject?.id },
|
variables: { appId: project?.id },
|
||||||
skip: !currentProject?.id,
|
skip: !project?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const passwordMinLength =
|
const passwordMinLength =
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { Container } from '@/components/layout/Container';
|
import { Container } from '@/components/layout/Container';
|
||||||
import { Modal } from '@/components/ui/v1/Modal';
|
import { Modal } from '@/components/ui/v1/Modal';
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
import { Button } from '@/components/ui/v2/Button';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
|
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
|
||||||
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
||||||
import { ApplicationLockedReason } from '@/features/orgs/projects/common/components/ApplicationLockedReason';
|
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||||
import { ApplicationPausedReason } from '@/features/orgs/projects/common/components/ApplicationPausedReason';
|
|
||||||
import { ApplicationPausedSymbol } from '@/features/orgs/projects/common/components/ApplicationPausedSymbol';
|
|
||||||
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
|
||||||
import { StagingMetadata } from '@/features/orgs/projects/common/components/StagingMetadata';
|
import { StagingMetadata } from '@/features/orgs/projects/common/components/StagingMetadata';
|
||||||
import { useAppPausedReason } from '@/features/orgs/projects/common/hooks/useAppPausedReason';
|
|
||||||
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/useIsCurrentUserOwner';
|
||||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
@@ -19,46 +13,19 @@ import {
|
|||||||
GetAllWorkspacesAndProjectsDocument,
|
GetAllWorkspacesAndProjectsDocument,
|
||||||
useUnpauseApplicationMutation,
|
useUnpauseApplicationMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function ApplicationPaused() {
|
export default function ApplicationPaused() {
|
||||||
const { org } = useCurrentOrg();
|
const { org } = useCurrentOrg();
|
||||||
const { project, refetch: refetchProject } = useProject();
|
const { project } = useProject();
|
||||||
const isOwner = useIsCurrentUserOwner();
|
const isOwner = useIsCurrentUserOwner();
|
||||||
const [transferProjectDialogOpen, setTransferProjectDialogOpen] =
|
const [transferProjectDialogOpen, setTransferProjectDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
useUnpauseApplicationMutation({
|
||||||
useUnpauseApplicationMutation({
|
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { isLocked, lockedReason, freeAndLiveProjectsNumberExceeded, loading } =
|
|
||||||
useAppPausedReason();
|
|
||||||
|
|
||||||
async function handleTriggerUnpausing() {
|
|
||||||
await execPromiseWithErrorToast(
|
|
||||||
async () => {
|
|
||||||
await unpauseApplication({ variables: { appId: project.id } });
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
});
|
|
||||||
await refetchProject();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loadingMessage: 'Starting the project...',
|
|
||||||
successMessage: 'The project has been started successfully.',
|
|
||||||
errorMessage:
|
|
||||||
'An error occurred while waking up the project. Please try again.',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <ActivityIndicator label="Loading user data..." delay={1000} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -77,65 +44,38 @@ export default function ApplicationPaused() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Container className="mx-auto grid max-w-lg grid-flow-row gap-6 text-center">
|
<Container className="mx-auto grid max-w-lg grid-flow-row gap-6 text-center">
|
||||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
<div className="mx-auto flex w-full max-w-xs flex-col gap-4">
|
||||||
<ApplicationPausedSymbol isLocked={isLocked} />
|
<ApplicationPausedBanner
|
||||||
</div>
|
alertClassName="items-center"
|
||||||
|
textContainerClassName="items-center text-center"
|
||||||
<Box className="grid grid-flow-row gap-6">
|
/>
|
||||||
<Text variant="h3" component="h1">
|
{org && (
|
||||||
{project.name} is {isLocked ? 'locked' : 'paused'}
|
|
||||||
</Text>
|
|
||||||
{isLocked ? (
|
|
||||||
<ApplicationLockedReason reason={lockedReason} />
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<ApplicationPausedReason
|
<Button
|
||||||
freeAndLiveProjectsNumberExceeded={
|
className="w-full"
|
||||||
freeAndLiveProjectsNumberExceeded
|
onClick={() => setTransferProjectDialogOpen(true)}
|
||||||
}
|
>
|
||||||
/>
|
Transfer
|
||||||
<div className="grid grid-flow-row gap-4">
|
</Button>
|
||||||
{org && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
className="mx-auto w-full max-w-xs"
|
|
||||||
onClick={() => setTransferProjectDialogOpen(true)}
|
|
||||||
>
|
|
||||||
Transfer
|
|
||||||
</Button>
|
|
||||||
<TransferProjectDialog
|
|
||||||
open={transferProjectDialogOpen}
|
|
||||||
setOpen={setTransferProjectDialogOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="borderless"
|
|
||||||
className="mx-auto w-full max-w-xs"
|
|
||||||
loading={changingApplicationStateLoading}
|
|
||||||
disabled={
|
|
||||||
changingApplicationStateLoading ||
|
|
||||||
freeAndLiveProjectsNumberExceeded
|
|
||||||
}
|
|
||||||
onClick={handleTriggerUnpausing}
|
|
||||||
>
|
|
||||||
Wake Up
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isOwner && (
|
<TransferProjectDialog
|
||||||
<Button
|
open={transferProjectDialogOpen}
|
||||||
color="error"
|
setOpen={setTransferProjectDialogOpen}
|
||||||
variant="outlined"
|
/>
|
||||||
className="mx-auto w-full max-w-xs"
|
|
||||||
onClick={() => setShowDeletingModal(true)}
|
|
||||||
>
|
|
||||||
Delete Project
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
|
||||||
|
{isOwner && (
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
className="mx-auto w-full max-w-xs"
|
||||||
|
onClick={() => setShowDeletingModal(true)}
|
||||||
|
>
|
||||||
|
Delete Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<StagingMetadata>
|
<StagingMetadata>
|
||||||
<ApplicationInfo />
|
<ApplicationInfo />
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||||
|
import { Alert } from '@/components/ui/v2/Alert';
|
||||||
|
import { Button } from '@/components/ui/v3/button';
|
||||||
|
import { useAppPausedReason } from '@/features/orgs/projects/common/hooks/useAppPausedReason';
|
||||||
|
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||||
|
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { ApplicationStatus } from '@/types/application';
|
||||||
|
import { useUnpauseApplicationMutation } from '@/utils/__generated__/graphql';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export default function ApplicationPausedBanner({
|
||||||
|
alertClassName,
|
||||||
|
textContainerClassName,
|
||||||
|
wakeUpButtonClassName,
|
||||||
|
}: {
|
||||||
|
alertClassName?: string;
|
||||||
|
textContainerClassName?: string;
|
||||||
|
wakeUpButtonClassName?: string;
|
||||||
|
}) {
|
||||||
|
const { org } = useCurrentOrg();
|
||||||
|
const { state } = useAppState();
|
||||||
|
const { freeAndLiveProjectsNumberExceeded } = useAppPausedReason();
|
||||||
|
const { project, refetch: refetchProject } = useProject();
|
||||||
|
|
||||||
|
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||||
|
useUnpauseApplicationMutation({
|
||||||
|
variables: {
|
||||||
|
appId: project?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTriggerUnpausing = useCallback(async () => {
|
||||||
|
await execPromiseWithErrorToast(
|
||||||
|
async () => {
|
||||||
|
await unpauseApplication({ variables: { appId: project.id } });
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
await refetchProject();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loadingMessage: 'Starting the project...',
|
||||||
|
successMessage: 'The project has been started successfully.',
|
||||||
|
errorMessage:
|
||||||
|
'An error occurred while waking up the project. Please try again.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [unpauseApplication, project?.id, refetchProject]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
className={cn(
|
||||||
|
'flex w-full flex-col items-start justify-between gap-4 p-4',
|
||||||
|
alertClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/PausedApp.svg"
|
||||||
|
className="mt-1"
|
||||||
|
alt="Closed Eye"
|
||||||
|
width={52}
|
||||||
|
height={40}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-full w-full flex-col gap-2',
|
||||||
|
textContainerClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="w-full">
|
||||||
|
Project <b>{project?.name}</b> is paused.
|
||||||
|
</p>
|
||||||
|
<p className="w-full">
|
||||||
|
Wake up your project to make it accessible again. Once reactivated,
|
||||||
|
all features will be fully functional. Go to settings to manage your
|
||||||
|
project.
|
||||||
|
</p>
|
||||||
|
{org?.plan?.isFree && (
|
||||||
|
<p>
|
||||||
|
Projects under your Personal Organization will stop responding to
|
||||||
|
API calls after 7 days of inactivity, so consider transferring the
|
||||||
|
project to a <b>Pro Organization</b> to avoid auto-sleep.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{freeAndLiveProjectsNumberExceeded && (
|
||||||
|
<p>
|
||||||
|
Additionally, only 1 free project can be active at any given time,
|
||||||
|
so please pause your current active free project before unpausing
|
||||||
|
another.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{state === ApplicationStatus.Paused && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn('w-full', wakeUpButtonClassName)}
|
||||||
|
disabled={changingApplicationStateLoading}
|
||||||
|
onClick={handleTriggerUnpausing}
|
||||||
|
>
|
||||||
|
{changingApplicationStateLoading ? <ActivityIndicator /> : 'Wake up'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ApplicationPausedBanner } from './ApplicationPausedBanner';
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Alert } from '@/components/ui/v2/Alert';
|
|
||||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
|
||||||
|
|
||||||
interface ApplicationPausedReasonProps {
|
|
||||||
freeAndLiveProjectsNumberExceeded?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ApplicationPausedReason({
|
|
||||||
freeAndLiveProjectsNumberExceeded,
|
|
||||||
}: ApplicationPausedReasonProps) {
|
|
||||||
const { org } = useCurrentOrg();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
className="flex flex-col w-full max-w-xs gap-4 p-6 mx-auto text-left"
|
|
||||||
>
|
|
||||||
{org?.plan?.isFree ? (
|
|
||||||
<p>
|
|
||||||
Projects under your Personal Organization will stop responding to API
|
|
||||||
calls after 7 days of inactivity, so consider transferring the project
|
|
||||||
to a <b>Pro Organization</b> to avoid auto-sleep.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-center">Your project is Paused.</p>
|
|
||||||
)}
|
|
||||||
{freeAndLiveProjectsNumberExceeded && (
|
|
||||||
<p className="text-center">
|
|
||||||
Additionally, only 1 free project can be active at any given time, so
|
|
||||||
please pause your current active free project before unpausing
|
|
||||||
another.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as ApplicationPausedReason } from './ApplicationPausedReason';
|
|
||||||
@@ -48,7 +48,7 @@ export default function StagingMetadata({
|
|||||||
}: PropsWithChildren<unknown>) {
|
}: PropsWithChildren<unknown>) {
|
||||||
return (
|
return (
|
||||||
isDevOrStaging() && (
|
isDevOrStaging() && (
|
||||||
<div className="mx-auto mt-10 max-w-sm">
|
<div className="mx-auto max-w-sm">
|
||||||
<Box className="mx-auto grid grid-flow-row justify-items-center rounded-md border p-5 text-center">
|
<Box className="mx-auto grid grid-flow-row justify-items-center rounded-md border p-5 text-center">
|
||||||
<Status status={StatusEnum.Deploying}>Internal info</Status>
|
<Status status={StatusEnum.Deploying}>Internal info</Status>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
import { useUI } from '@/components/common/UIProvider';
|
|
||||||
import { StateBadge } from '@/components/presentational/StateBadge';
|
|
||||||
import type { DeploymentStatus } from '@/components/presentational/StatusCircle';
|
|
||||||
import { StatusCircle } from '@/components/presentational/StatusCircle';
|
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
|
||||||
import type { ButtonProps } from '@/components/ui/v2/Button';
|
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
|
||||||
import { Divider } from '@/components/ui/v2/Divider';
|
|
||||||
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
|
||||||
import { SearchIcon } from '@/components/ui/v2/icons/SearchIcon';
|
|
||||||
import type { InputProps } from '@/components/ui/v2/Input';
|
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
|
||||||
import { Link } from '@/components/ui/v2/Link';
|
|
||||||
import { List } from '@/components/ui/v2/List';
|
|
||||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { DeploymentStatusMessage } from '@/features/projects/deployments/components/DeploymentStatusMessage';
|
|
||||||
import type { ApplicationState, Workspace } from '@/types/application';
|
|
||||||
import { ApplicationStatus } from '@/types/application';
|
|
||||||
import { getApplicationStatusString } from '@/utils/helpers';
|
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import NavLink from 'next/link';
|
|
||||||
import type { ChangeEvent, PropsWithoutRef } from 'react';
|
|
||||||
import { Fragment, useState } from 'react';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
export interface WorkspaceAndProjectListProps extends BoxProps {
|
|
||||||
/**
|
|
||||||
* List of workspaces to be displayed.
|
|
||||||
*/
|
|
||||||
workspaces: Workspace[];
|
|
||||||
/**
|
|
||||||
* Props to be passed to individual slots.
|
|
||||||
*/
|
|
||||||
slotProps?: {
|
|
||||||
root?: BoxProps;
|
|
||||||
header?: BoxProps;
|
|
||||||
search?: PropsWithoutRef<InputProps>;
|
|
||||||
button?: PropsWithoutRef<ButtonProps>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkStatusOfTheApplication(stateHistory: ApplicationState[] | []) {
|
|
||||||
if (stateHistory.length === 0) {
|
|
||||||
return ApplicationStatus.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateHistory[0].stateId === undefined) {
|
|
||||||
return ApplicationStatus.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stateHistory[0].stateId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WorkspaceAndProjectList({
|
|
||||||
workspaces,
|
|
||||||
className,
|
|
||||||
slotProps = {},
|
|
||||||
...props
|
|
||||||
}: WorkspaceAndProjectListProps) {
|
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
const { maintenanceActive } = useUI();
|
|
||||||
|
|
||||||
const handleQueryChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
slotProps?.search?.onChange?.(event);
|
|
||||||
setQuery(event.target.value);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const filteredWorkspaces = workspaces
|
|
||||||
.map((workspace) => ({
|
|
||||||
...workspace,
|
|
||||||
projects: workspace.projects.filter((project) =>
|
|
||||||
project.name.toLowerCase().includes(query.toLowerCase()),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter((workspace) => workspace.projects.length > 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
{...props}
|
|
||||||
{...slotProps.root}
|
|
||||||
className={twMerge(
|
|
||||||
'grid grid-flow-row content-start gap-4',
|
|
||||||
className,
|
|
||||||
slotProps.root?.className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
{...slotProps.header}
|
|
||||||
className={twMerge(
|
|
||||||
'grid grid-flow-col place-content-between items-center',
|
|
||||||
slotProps.header?.className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Text variant="h2" component="h1" className="hidden md:block">
|
|
||||||
My Projects
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
placeholder="Find Project"
|
|
||||||
startAdornment={
|
|
||||||
<SearchIcon
|
|
||||||
className="w-4 h-4 ml-2 -mr-1 shrink-0"
|
|
||||||
sx={{ color: 'text.disabled' }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{...slotProps.search}
|
|
||||||
onChange={handleQueryChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NavLink href="/new" passHref legacyBehavior>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<PlusCircleIcon />}
|
|
||||||
disabled={maintenanceActive}
|
|
||||||
{...slotProps.button}
|
|
||||||
>
|
|
||||||
New Project
|
|
||||||
</Button>
|
|
||||||
</NavLink>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box className="grid grid-flow-row gap-8 my-8">
|
|
||||||
{filteredWorkspaces.map((workspace) => (
|
|
||||||
<div key={workspace.slug}>
|
|
||||||
<NavLink href={`/${workspace.slug}`} passHref legacyBehavior>
|
|
||||||
<Link
|
|
||||||
href={`${workspace.slug}`}
|
|
||||||
className="mb-1.5 block font-medium"
|
|
||||||
underline="none"
|
|
||||||
sx={{ color: 'text.primary' }}
|
|
||||||
>
|
|
||||||
{workspace.name}
|
|
||||||
</Link>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<List className="grid grid-flow-row border-y">
|
|
||||||
{workspace.projects.map((project, index) => {
|
|
||||||
const [latestDeployment] = project.deployments;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={project.slug}>
|
|
||||||
<ListItem.Root
|
|
||||||
secondaryAction={
|
|
||||||
<div className="grid grid-flow-col gap-px">
|
|
||||||
{latestDeployment && (
|
|
||||||
<div className="flex self-center mr-2 align-middle">
|
|
||||||
<StatusCircle
|
|
||||||
status={
|
|
||||||
latestDeployment.deploymentStatus as DeploymentStatus
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<StateBadge
|
|
||||||
state={checkStatusOfTheApplication(
|
|
||||||
project.appStates,
|
|
||||||
)}
|
|
||||||
desiredState={project.desiredState}
|
|
||||||
title={getApplicationStatusString(
|
|
||||||
checkStatusOfTheApplication(project.appStates),
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
href={`${workspace?.slug}/${project.slug}`}
|
|
||||||
passHref
|
|
||||||
className='w-full'
|
|
||||||
legacyBehavior>
|
|
||||||
<ListItem.Button className="rounded-none">
|
|
||||||
<ListItem.Avatar>
|
|
||||||
<div className="w-10 h-10 overflow-hidden rounded-lg">
|
|
||||||
<Image
|
|
||||||
src="/logos/new.svg"
|
|
||||||
alt="Nhost Logo"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ListItem.Avatar>
|
|
||||||
|
|
||||||
<ListItem.Text
|
|
||||||
primary={project.name}
|
|
||||||
secondary={
|
|
||||||
<DeploymentStatusMessage
|
|
||||||
appCreatedAt={project.createdAt}
|
|
||||||
deployment={latestDeployment}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem.Button>
|
|
||||||
</NavLink>
|
|
||||||
</ListItem.Root>
|
|
||||||
|
|
||||||
{index < workspace.projects.length - 1 && (
|
|
||||||
<Divider component="li" role="listitem" />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './WorkspaceAndProjectList';
|
|
||||||
export { default as WorkspaceAndProjectList } from './WorkspaceAndProjectList';
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { useTheme } from '@mui/material';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
export interface ResourceProps {
|
|
||||||
text: string;
|
|
||||||
logo: string;
|
|
||||||
link: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Resource({ text, logo, link }: ResourceProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="flex justify-between py-1 align-middle"
|
|
||||||
>
|
|
||||||
<div className="flex items-center align-middle">
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
theme.palette.mode === 'dark'
|
|
||||||
? `/logos/light/${logo}.svg`
|
|
||||||
: `/logos/${logo}.svg`
|
|
||||||
}
|
|
||||||
alt={text}
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text className="ml-2 inline-flex self-center align-middle font-medium">
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="flex self-center">
|
|
||||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
|
||||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
|
||||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
|
||||||
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
|
|
||||||
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
|
||||||
import { List } from '@/components/ui/v2/List';
|
|
||||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
|
||||||
import { Announcements } from '@/features/projects/common/components/Announcements';
|
|
||||||
import { EditWorkspaceNameForm } from '@/features/projects/workspaces/components/EditWorkspaceNameForm';
|
|
||||||
import type { Workspace } from '@/types/application';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import NavLink from 'next/link';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
import Resource from './Resource';
|
|
||||||
|
|
||||||
export interface WorkspaceSidebarProps extends BoxProps {
|
|
||||||
/**
|
|
||||||
* List of workspaces to be displayed.
|
|
||||||
*/
|
|
||||||
workspaces: Workspace[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WorkspaceSidebar({
|
|
||||||
className,
|
|
||||||
workspaces,
|
|
||||||
...props
|
|
||||||
}: WorkspaceSidebarProps) {
|
|
||||||
const { openDialog } = useDialog();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
component="aside"
|
|
||||||
className={twMerge(
|
|
||||||
'grid w-full grid-flow-row content-start gap-8 md:grid',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Announcements />
|
|
||||||
|
|
||||||
<section className="grid grid-flow-row gap-2">
|
|
||||||
<Text color="secondary">My Workspaces</Text>
|
|
||||||
|
|
||||||
{workspaces.length > 0 ? (
|
|
||||||
<List className="grid grid-flow-row gap-2">
|
|
||||||
{workspaces.map(({ id, name, slug }) => (
|
|
||||||
<ListItem.Root key={id}>
|
|
||||||
<NavLink href={`/${slug}`} passHref className='w-full' legacyBehavior>
|
|
||||||
<ListItem.Button
|
|
||||||
dense
|
|
||||||
aria-label={`View ${name}`}
|
|
||||||
className="!p-1"
|
|
||||||
>
|
|
||||||
<ListItem.Avatar className="w-8 h-8">
|
|
||||||
<div className="inline-block w-8 h-8 overflow-hidden rounded-lg">
|
|
||||||
<Image
|
|
||||||
src="/logos/new.svg"
|
|
||||||
alt="Nhost Logo"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ListItem.Avatar>
|
|
||||||
<ListItem.Text primary={name} />
|
|
||||||
</ListItem.Button>
|
|
||||||
</NavLink>
|
|
||||||
</ListItem.Root>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
) : (
|
|
||||||
<ActivityIndicator
|
|
||||||
label="Creating your first workspace..."
|
|
||||||
className="py-1"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="borderless"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<PlusCircleIcon />}
|
|
||||||
className="justify-self-start"
|
|
||||||
onClick={() => {
|
|
||||||
openDialog({
|
|
||||||
title: (
|
|
||||||
<span className="grid grid-flow-row">
|
|
||||||
<span>New Workspace</span>
|
|
||||||
|
|
||||||
<Text variant="subtitle1" component="span">
|
|
||||||
Invite team members to workspaces to work collaboratively.
|
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
component: <EditWorkspaceNameForm />,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
New Workspace
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-flow-row gap-2">
|
|
||||||
<Text color="secondary">Resources</Text>
|
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
|
||||||
<Resource
|
|
||||||
text="Documentation"
|
|
||||||
logo="Note"
|
|
||||||
link="https://docs.nhost.io"
|
|
||||||
/>
|
|
||||||
<Resource
|
|
||||||
text="JavaScript Client"
|
|
||||||
logo="js"
|
|
||||||
link="https://docs.nhost.io/reference/javascript/"
|
|
||||||
/>
|
|
||||||
<Resource
|
|
||||||
text="Nhost CLI"
|
|
||||||
logo="CLI"
|
|
||||||
link="https://docs.nhost.io/platform/cli"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid grid-flow-row gap-2">
|
|
||||||
<NavLink
|
|
||||||
href="https://github.com/nhost/nhost"
|
|
||||||
passHref
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
legacyBehavior>
|
|
||||||
<Button
|
|
||||||
className="grid w-full grid-flow-col gap-1"
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
startIcon={<GitHubIcon />}
|
|
||||||
>
|
|
||||||
Star us on GitHub
|
|
||||||
</Button>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink
|
|
||||||
href="https://discord.com/invite/9V7Qb2U"
|
|
||||||
passHref
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
legacyBehavior>
|
|
||||||
<Button
|
|
||||||
className="grid w-full grid-flow-col gap-1"
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
aria-labelledby="discord-button-label"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src="/assets/brands/discord.svg"
|
|
||||||
alt="Discord Logo"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span id="discord-button-label">Join Discord</span>
|
|
||||||
</Button>
|
|
||||||
</NavLink>
|
|
||||||
</section>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as WorkspaceSidebar } from './WorkspaceSidebar';
|
|
||||||
@@ -159,9 +159,6 @@ export default function TOMLEditor() {
|
|||||||
height="100%"
|
height="100%"
|
||||||
width="100%"
|
width="100%"
|
||||||
theme={theme.palette.mode === 'light' ? bbedit : githubDark}
|
theme={theme.palette.mode === 'light' ? bbedit : githubDark}
|
||||||
basicSetup={{
|
|
||||||
searchKeymap: false,
|
|
||||||
}}
|
|
||||||
extensions={[StreamLanguage.define(toml)]}
|
extensions={[StreamLanguage.define(toml)]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function useAppPausedReason(): {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: isLockedData } = useGetProjectIsLockedQuery({
|
const { data: isLockedData } = useGetProjectIsLockedQuery({
|
||||||
variables: { appId: project.id },
|
variables: { appId: project?.id },
|
||||||
skip: !project,
|
skip: !project,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import { Text } from '@/components/ui/v2/Text';
|
|||||||
import type {
|
import type {
|
||||||
ColumnType,
|
ColumnType,
|
||||||
DatabaseColumn,
|
DatabaseColumn,
|
||||||
} from '@/features/database/dataGrid/types/dataBrowser';
|
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
import {
|
import {
|
||||||
identityTypes,
|
identityTypes,
|
||||||
postgresFunctions,
|
postgresFunctions,
|
||||||
postgresTypeGroups,
|
postgresTypeGroups,
|
||||||
} from '@/features/database/dataGrid/utils/postgresqlConstants';
|
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
||||||
import type { DialogFormProps } from '@/types/common';
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -102,7 +102,7 @@ export default function BaseColumnForm({
|
|||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleExternalSubmit}
|
onSubmit={handleExternalSubmit}
|
||||||
className="flex flex-col content-between flex-auto overflow-hidden border-t-1"
|
className="flex flex-auto flex-col content-between overflow-hidden border-t-1"
|
||||||
>
|
>
|
||||||
<div className="flex-auto overflow-y-auto">
|
<div className="flex-auto overflow-y-auto">
|
||||||
<section className="grid grid-cols-8 px-6 py-3">
|
<section className="grid grid-cols-8 px-6 py-3">
|
||||||
@@ -184,7 +184,7 @@ export default function BaseColumnForm({
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
className="w-full col-span-8 py-3 m-0 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
className="col-span-8 m-0 w-full py-3 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
||||||
onChange={(_event, checked) => {
|
onChange={(_event, checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setDefaultValueInputText('');
|
setDefaultValueInputText('');
|
||||||
@@ -197,7 +197,7 @@ export default function BaseColumnForm({
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="section"
|
component="section"
|
||||||
className="grid grid-cols-8 px-6 py-3 border-t-1"
|
className="grid grid-cols-8 border-t-1 px-6 py-3"
|
||||||
>
|
>
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
id="defaultValue"
|
id="defaultValue"
|
||||||
@@ -249,7 +249,7 @@ export default function BaseColumnForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
className="w-full col-span-8 py-3 m-0 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
className="col-span-8 m-0 w-full py-3 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
||||||
name="isNullable"
|
name="isNullable"
|
||||||
label={
|
label={
|
||||||
<span className="inline-grid grid-flow-row">
|
<span className="inline-grid grid-flow-row">
|
||||||
@@ -269,7 +269,7 @@ export default function BaseColumnForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
className="w-full col-span-8 py-3 m-0 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
className="col-span-8 m-0 w-full py-3 sm:col-span-6 sm:col-start-3 sm:ml-1"
|
||||||
name="isUnique"
|
name="isUnique"
|
||||||
label={
|
label={
|
||||||
<span className="inline-grid grid-flow-row">
|
<span className="inline-grid grid-flow-row">
|
||||||
@@ -306,7 +306,7 @@ export default function BaseColumnForm({
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box className="grid justify-between flex-shrink-0 grid-flow-col gap-3 p-2 border-t-1">
|
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
|||||||
import { LinkIcon } from '@/components/ui/v2/icons/LinkIcon';
|
import { LinkIcon } from '@/components/ui/v2/icons/LinkIcon';
|
||||||
import { InputLabel } from '@/components/ui/v2/InputLabel';
|
import { InputLabel } from '@/components/ui/v2/InputLabel';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
import { CreateForeignKeyForm } from '@/features/database/dataGrid/components/CreateForeignKeyForm';
|
import { CreateForeignKeyForm } from '@/features/orgs/projects/database/dataGrid/components/CreateForeignKeyForm';
|
||||||
import { EditForeignKeyForm } from '@/features/database/dataGrid/components/EditForeignKeyForm';
|
import { EditForeignKeyForm } from '@/features/orgs/projects/database/dataGrid/components/EditForeignKeyForm';
|
||||||
import type { DatabaseColumn } from '@/features/database/dataGrid/types/dataBrowser';
|
import type { DatabaseColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||||
import type { ForwardedRef } from 'react';
|
import type { ForwardedRef } from 'react';
|
||||||
import { forwardRef, useRef } from 'react';
|
import { forwardRef, useRef } from 'react';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -106,7 +106,7 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
className="min-w-[initial] py-1 px-2"
|
className="min-w-[initial] px-2 py-1"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -114,7 +114,7 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setValue('foreignKeyRelation', null)}
|
onClick={() => setValue('foreignKeyRelation', null)}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
className="min-w-[initial] py-1 px-2"
|
className="min-w-[initial] px-2 py-1"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user