Compare commits
38 Commits
@nhost/das
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d43bd2c3b | ||
|
|
6f324afcae | ||
|
|
c1eff3a66b | ||
|
|
c774efee40 | ||
|
|
b36aa6041d | ||
|
|
9ed9857e17 | ||
|
|
b4b057e12c | ||
|
|
5b087257e4 | ||
|
|
f1b2117c37 | ||
|
|
c1514eb098 | ||
|
|
21bddeed6a | ||
|
|
84dd864186 | ||
|
|
e2c7741468 | ||
|
|
89fd97cbf0 | ||
|
|
f830a9d5f2 | ||
|
|
9a7e431323 | ||
|
|
1f2b0dced4 | ||
|
|
2c49961885 | ||
|
|
1e867b65d8 | ||
|
|
44f3f705c5 | ||
|
|
e5a50f79b1 | ||
|
|
0d89584268 | ||
|
|
b368f00a9a | ||
|
|
24c7b8a417 | ||
|
|
c3e200c55a | ||
|
|
8fb3064eea | ||
|
|
e5f1c6cb78 | ||
|
|
02994ee4e2 | ||
|
|
74a1239cd5 | ||
|
|
e32528bde5 | ||
|
|
ff4f210204 | ||
|
|
2fa9db428e | ||
|
|
6b9b2e4e6a | ||
|
|
61a8c6930f | ||
|
|
8f169885f7 | ||
|
|
6104e72204 | ||
|
|
65ca5deb4c | ||
|
|
e42832a012 |
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_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_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_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "gen: update depenendencies"
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 1 * *'
|
||||
- cron: '0 2 1 2,5,8,11 *'
|
||||
|
||||
jobs:
|
||||
run:
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { navigateToProject } from '@/e2e/utils';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /ai/i })
|
||||
.click();
|
||||
const AIRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/ai/assistants`;
|
||||
await page.goto(AIRoute);
|
||||
await page.waitForURL(AIRoute);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { navigateToProject } from '@/e2e/utils';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /ai/i })
|
||||
.click();
|
||||
const AIRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/ai/auto-embeddings`;
|
||||
await page.goto(AIRoute);
|
||||
await page.waitForURL(AIRoute);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import test, { expect } from '@playwright/test';
|
||||
|
||||
test('should be able to ban and unban a user', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
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 authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||
await page.goto(authUrl);
|
||||
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import test, { expect } from '@playwright/test';
|
||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
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 authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||
await page.goto(authUrl);
|
||||
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import test, { expect } from '@playwright/test';
|
||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
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 authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||
await page.goto(authUrl);
|
||||
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { createUser, generateTestEmail } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
@@ -15,23 +11,9 @@ test.beforeAll(async ({ browser }) => {
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
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 authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||
await page.goto(authUrl);
|
||||
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject, prepareTable } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { navigateToProject, prepareTable } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
@@ -18,17 +14,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /database/i })
|
||||
.click();
|
||||
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
|
||||
await page.goto(databaseRoute);
|
||||
await page.waitForURL(databaseRoute);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
@@ -55,7 +49,7 @@ test('should create a simple table', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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(
|
||||
@@ -84,7 +78,7 @@ test('should create a table with unique constraints', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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(
|
||||
@@ -113,7 +107,7 @@ test('should create a table with nullable columns', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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(
|
||||
@@ -146,7 +140,7 @@ test('should create a table with an identity column', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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(
|
||||
@@ -174,7 +168,7 @@ test('should create table with foreign key constraint', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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();
|
||||
@@ -219,7 +213,7 @@ test('should create table with foreign key constraint', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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(
|
||||
@@ -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.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();
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { deleteTable, navigateToProject, prepareTable } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
@@ -18,17 +14,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /database/i })
|
||||
.click();
|
||||
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
|
||||
await page.goto(databaseRoute);
|
||||
await page.waitForURL(databaseRoute);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
@@ -53,7 +47,7 @@ test('should delete a table', async () => {
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
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({
|
||||
@@ -63,7 +57,7 @@ test('should delete a table', async () => {
|
||||
|
||||
// navigate to next URL
|
||||
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(
|
||||
@@ -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.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();
|
||||
@@ -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.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(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import slugify from 'slugify';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
});
|
||||
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_NAME,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { navigateToProject } from '../utils';
|
||||
|
||||
let page: Page;
|
||||
|
||||
@@ -14,11 +9,11 @@ test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
await openProject({
|
||||
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,60 +21,34 @@ test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should show a sidebar with menu items', async () => {
|
||||
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
|
||||
test('should show the navtree with all links visible', async () => {
|
||||
const navLocator = page.getByLabel('Navigation Tree');
|
||||
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 () => {
|
||||
await expect(
|
||||
page.getByRole('banner').getByRole('link', { name: TEST_WORKSPACE_NAME }),
|
||||
).toBeVisible();
|
||||
const links = [
|
||||
'Nhost Automation Test Project',
|
||||
'Overview',
|
||||
'Database',
|
||||
'GraphQL',
|
||||
'Hasura',
|
||||
'Auth',
|
||||
'Storage',
|
||||
'Run',
|
||||
'AI',
|
||||
'Deployments',
|
||||
'Backups',
|
||||
'Logs',
|
||||
'Metrics',
|
||||
'Settings',
|
||||
];
|
||||
|
||||
await expect(
|
||||
page.getByRole('banner').getByRole('link', { name: TEST_PROJECT_NAME }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the project's name, the Upgrade button and the Settings button", async () => {
|
||||
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();
|
||||
for (const linkName of links) {
|
||||
const link =
|
||||
linkName === 'Settings'
|
||||
? page.getByRole('link', { name: linkName }).first()
|
||||
: page.getByRole('link', { name: linkName });
|
||||
await expect(link).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
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(),
|
||||
).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 {
|
||||
PRO_TEST_PROJECT_NAME,
|
||||
PRO_TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { navigateToProject } from '../utils';
|
||||
|
||||
let page: Page;
|
||||
|
||||
@@ -16,17 +12,15 @@ test.beforeAll(async ({ browser }) => {
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: PRO_TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: PRO_TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /run/i })
|
||||
.click();
|
||||
const runRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/run`;
|
||||
await page.goto(runRoute);
|
||||
await page.waitForURL(runRoute);
|
||||
});
|
||||
|
||||
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).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 expect(
|
||||
@@ -47,12 +45,6 @@ test('should create and delete a run service', async () => {
|
||||
|
||||
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 page.getByLabel(/more options/i).click();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
TEST_DASHBOARD_URL,
|
||||
TEST_PERSONAL_ORG_SLUG,
|
||||
TEST_USER_EMAIL,
|
||||
TEST_USER_PASSWORD,
|
||||
} from '@/e2e/env';
|
||||
@@ -15,6 +16,9 @@ setup('authenticate user', async ({ page }) => {
|
||||
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
||||
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' });
|
||||
});
|
||||
|
||||
@@ -5,24 +5,27 @@ import type { Page } from '@playwright/test';
|
||||
* Open a project by navigating to the project's overview page.
|
||||
*
|
||||
* @param page - The Playwright page object.
|
||||
* @param workspaceSlug - The slug of the workspace that contains the project.
|
||||
* @param projectSlug - The slug of the project to open.
|
||||
* @param projectName - The name of the project to open.
|
||||
* @param orgSlug - The slug of the organization that contains the project.
|
||||
* @param projectSubdomain - The subdomain of the project to open.
|
||||
* @returns A promise that resolves when the project is opened.
|
||||
*/
|
||||
export async function openProject({
|
||||
export async function navigateToProject({
|
||||
page,
|
||||
projectName,
|
||||
workspaceSlug,
|
||||
projectSlug,
|
||||
orgSlug,
|
||||
projectSubdomain,
|
||||
}: {
|
||||
page: Page;
|
||||
workspaceSlug: string;
|
||||
projectSlug: string;
|
||||
projectName: string;
|
||||
orgSlug: string;
|
||||
projectSubdomain: string;
|
||||
}) {
|
||||
await page.getByRole('link', { name: projectName }).click();
|
||||
await page.waitForURL(`/${workspaceSlug}/${projectSlug}`);
|
||||
const projectUrl = `/orgs/${orgSlug}/projects/${projectSubdomain}`;
|
||||
|
||||
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 {
|
||||
TEST_DASHBOARD_URL,
|
||||
TEST_ORGANIZATION_SLUG,
|
||||
TEST_PROJECT_ADMIN_SECRET,
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { navigateToProject } from '@/e2e/utils';
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
async function globalTeardown() {
|
||||
@@ -18,13 +17,10 @@ async function globalTeardown() {
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
await navigateToProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
orgSlug: TEST_ORGANIZATION_SLUG,
|
||||
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
|
||||
});
|
||||
|
||||
const pagePromise = context.waitForEvent('page');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "2.0.5",
|
||||
"version": "2.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -42,6 +42,7 @@
|
||||
"@mui/x-date-pickers": "^5.0.20",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
@@ -127,7 +128,7 @@
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@next/bundle-analyzer": "^12.3.4",
|
||||
"@playwright/test": "1.41.0",
|
||||
"@playwright/test": "1.47.0",
|
||||
"@storybook/addon-actions": "^6.5.16",
|
||||
"@storybook/addon-essentials": "^6.5.16",
|
||||
"@storybook/addon-interactions": "^6.5.16",
|
||||
|
||||
@@ -6,7 +6,7 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30 * 1000,
|
||||
timeout: 40 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
@@ -20,6 +20,9 @@ export default defineConfig({
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
|
||||
launchOptions: {
|
||||
slowMo: 500,
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
|
||||
@@ -258,6 +258,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
className: twMerge(
|
||||
'max-w-md w-full',
|
||||
dialogProps?.PaperProps?.className,
|
||||
'z-30',
|
||||
),
|
||||
}}
|
||||
>
|
||||
@@ -288,6 +289,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawerWithDirtyGuard}
|
||||
SlideProps={{ onExited: clearDrawerContent, unmountOnExit: false }}
|
||||
className="z-40"
|
||||
PaperProps={{
|
||||
...drawerProps?.PaperProps,
|
||||
className: twMerge(
|
||||
|
||||
@@ -50,9 +50,9 @@ export default function UpgradeToProBanner({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2 gap-2">
|
||||
<div className="flex flex-col gap-2 space-y-2 lg:flex-row lg:items-center lg:space-x-2 lg:space-y-0">
|
||||
<Button
|
||||
className="lg:w-auto max-w-xs"
|
||||
className="max-w-xs lg:w-auto"
|
||||
onClick={() => {
|
||||
if (isOwner) {
|
||||
setTransferProjectDialogOpen(true);
|
||||
@@ -68,8 +68,6 @@ export default function UpgradeToProBanner({
|
||||
secondaryButtonText: 'I understand',
|
||||
hidePrimaryAction: true,
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -101,6 +99,7 @@ export default function UpgradeToProBanner({
|
||||
width={300}
|
||||
height={140}
|
||||
objectFit="contain"
|
||||
className=""
|
||||
alt="Upgrade to Pro illustration"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -18,6 +18,7 @@ import PinnedMainNav from '@/components/layout/MainNav/PinnedMainNav';
|
||||
import { OrgStatus } from '@/features/orgs/components/OrgStatus';
|
||||
import { useIsHealthy } from '@/features/orgs/projects/common/hooks/useIsHealthy';
|
||||
import { useNotFoundRedirect } from '@/features/projects/common/hooks/useNotFoundRedirect';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
@@ -90,7 +91,7 @@ export default function AuthenticatedLayout({
|
||||
|
||||
<Container
|
||||
rootClassName="h-full"
|
||||
className="grid justify-center max-w-md grid-flow-row gap-2 my-12 text-center"
|
||||
className="my-12 grid max-w-md grid-flow-row justify-center gap-2 text-center"
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
@@ -127,18 +128,23 @@ export default function AuthenticatedLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout className="flex flex-col h-full" {...props}>
|
||||
<BaseLayout className="flex h-full flex-col" {...props}>
|
||||
<Header className="flex py-1" />
|
||||
|
||||
<div
|
||||
className="relative flex flex-row h-full overflow-x-hidden"
|
||||
className="relative flex h-full flex-row overflow-hidden"
|
||||
ref={setMainNavContainer}
|
||||
>
|
||||
{mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
||||
|
||||
<div className="relative flex flex-row w-full h-full bg-accent">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full w-full flex-row bg-accent',
|
||||
mainNavPinned && isMdOrLarger ? 'overflow-x-auto' : '',
|
||||
)}
|
||||
>
|
||||
{(!mainNavPinned || !isMdOrLarger) && (
|
||||
<div className="flex justify-center w-6 h-full">
|
||||
<div className="flex h-full w-6 justify-center">
|
||||
<MainNav container={mainNavContainer} />
|
||||
</div>
|
||||
)}
|
||||
@@ -148,7 +154,7 @@ export default function AuthenticatedLayout({
|
||||
className: 'flex flex-col items-center',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<div className="flex h-full w-full flex-col overflow-auto">
|
||||
<OrgStatus />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,13 @@ import { Logo } from '@/components/presentational/Logo';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { DevAssistant } from '@/features/ai/DevAssistant';
|
||||
import { DevAssistant as WorkspaceProjectDevAssistant } from '@/features/ai/DevAssistant';
|
||||
import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray';
|
||||
import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray';
|
||||
import { DevAssistant } from '@/features/orgs/projects/ai/DevAssistant';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
|
||||
@@ -25,14 +28,14 @@ export interface HeaderProps
|
||||
|
||||
export default function Header({ className, ...props }: HeaderProps) {
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { openDrawer } = useDialog();
|
||||
|
||||
const { project } = useProject();
|
||||
const { currentProject: workspaceProject } = useCurrentWorkspaceAndProject();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
|
||||
const openDevAssistant = () => {
|
||||
// The dev assistant can be only answer questions related to a particular project
|
||||
if (!project) {
|
||||
if (!project && !workspaceProject) {
|
||||
toast.error('You need to be inside a project to open the Assistant', {
|
||||
style: getToastStyleProps().style,
|
||||
...getToastStyleProps().error,
|
||||
@@ -41,10 +44,17 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
openDrawer({
|
||||
title: <GraphiteIcon />,
|
||||
component: <DevAssistant />,
|
||||
});
|
||||
if (org && project) {
|
||||
openDrawer({
|
||||
title: <GraphiteIcon />,
|
||||
component: <DevAssistant />,
|
||||
});
|
||||
} else {
|
||||
openDrawer({
|
||||
title: <GraphiteIcon />,
|
||||
component: <WorkspaceProjectDevAssistant />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -57,15 +67,15 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
{...props}
|
||||
>
|
||||
<div className="w-6 h-6 mr-2">
|
||||
<Logo className="w-6 h-6 mx-auto cursor-pointer" />
|
||||
<div className="mr-2 h-6 w-6">
|
||||
<Logo className="mx-auto h-6 w-6 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<BreadcrumbNav />
|
||||
|
||||
<div className="items-center hidden grid-flow-col gap-1 sm:grid">
|
||||
<div className="hidden grid-flow-col items-center gap-1 sm:grid">
|
||||
<Button className="rounded-full" onClick={openDevAssistant}>
|
||||
<GraphiteIcon className="w-4 h-4" />
|
||||
<GraphiteIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<NotificationsTray />
|
||||
|
||||
@@ -126,9 +126,10 @@ export default function OrgsComboBox() {
|
||||
<CommandGroup heading="Organizations">
|
||||
{orgsOptions.map((option) => (
|
||||
<CommandItem
|
||||
keywords={[option.label]}
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
className="flex items-center justify-between bg-background text-foreground dark:hover:bg-muted"
|
||||
value={option.value}
|
||||
className="flex items-center text-foreground dark:hover:bg-muted"
|
||||
onSelect={() => {
|
||||
setSelectedItem(option);
|
||||
setOpen(false);
|
||||
@@ -143,17 +144,15 @@ export default function OrgsComboBox() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedItem?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="truncate max-w-52">{option.label}</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedItem?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="w-full truncate">{option.label}</span>
|
||||
{renderBadge(option.plan)}
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -166,9 +165,10 @@ export default function OrgsComboBox() {
|
||||
<CommandGroup heading="Workspaces">
|
||||
{workspacesOptions.map((option) => (
|
||||
<CommandItem
|
||||
keywords={[option.label]}
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
className="flex items-center justify-between bg-background text-foreground hover:bg-accent dark:hover:bg-muted"
|
||||
value={option.value}
|
||||
className="flex items-center text-foreground dark:hover:bg-muted"
|
||||
onSelect={() => {
|
||||
setSelectedItem(option);
|
||||
setOpen(false);
|
||||
@@ -183,19 +183,15 @@ export default function OrgsComboBox() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedItem?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="truncate max-w-52">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
selectedItem?.value === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<span className="w-full truncate">{option.label}</span>
|
||||
{renderBadge(option.plan)}
|
||||
</CommandItem>
|
||||
))}
|
||||
|
||||
@@ -68,6 +68,7 @@ const projectSettingsPages = [
|
||||
route: 'rate-limiting',
|
||||
},
|
||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
].map((item) => ({
|
||||
label: item.name,
|
||||
|
||||
@@ -62,14 +62,14 @@ export default function ProjectsComboBox() {
|
||||
>
|
||||
{selectedProject ? (
|
||||
<div className="flex flex-row items-center justify-center gap-1">
|
||||
<Box className="w-4 h-4" />
|
||||
<Box className="h-4 w-4" />
|
||||
{selectedProject.label}
|
||||
<ProjectStatus />
|
||||
</div>
|
||||
) : (
|
||||
<>Select a project</>
|
||||
)}
|
||||
<ChevronsUpDown className="w-5 h-5 text-muted-foreground" />
|
||||
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
@@ -80,8 +80,9 @@ export default function ProjectsComboBox() {
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
keywords={[option.label]}
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
value={option.value}
|
||||
onSelect={() => {
|
||||
setSelectedProject(option);
|
||||
setOpen(false);
|
||||
@@ -97,8 +98,8 @@ export default function ProjectsComboBox() {
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<Box className="w-4 h-4" />
|
||||
<span className="truncate max-w-52">{option.label}</span>
|
||||
<Box className="h-4 w-4" />
|
||||
<span className="max-w-52 truncate">{option.label}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
|
||||
@@ -151,6 +151,7 @@ const projectSettingsPages = [
|
||||
route: 'rate-limiting',
|
||||
},
|
||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
];
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function PinnedMainNav() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full border-r p-0 sm:max-w-[310px]">
|
||||
<div className="flex h-full w-full flex-shrink-0 flex-col border-r p-0 sm:max-w-[310px]">
|
||||
<div className="flex justify-end w-full h-12 p-1 border-b bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -37,73 +37,73 @@ import { useTreeNavState } from './TreeNavStateContext';
|
||||
const projectPages = [
|
||||
{
|
||||
name: 'Overview',
|
||||
icon: <HomeIcon className="w-4 h-4" />,
|
||||
icon: <HomeIcon className="h-4 w-4" />,
|
||||
route: '',
|
||||
slug: 'overview',
|
||||
},
|
||||
{
|
||||
name: 'Database',
|
||||
icon: <DatabaseIcon className="w-4 h-4" />,
|
||||
icon: <DatabaseIcon className="h-4 w-4" />,
|
||||
route: 'database/browser/default',
|
||||
slug: 'database',
|
||||
},
|
||||
{
|
||||
name: 'GraphQL',
|
||||
icon: <GraphQLIcon className="w-4 h-4" />,
|
||||
icon: <GraphQLIcon className="h-4 w-4" />,
|
||||
route: 'graphql',
|
||||
slug: 'graphql',
|
||||
},
|
||||
{
|
||||
name: 'Hasura',
|
||||
icon: <HasuraIcon className="w-4 h-4" />,
|
||||
icon: <HasuraIcon className="h-4 w-4" />,
|
||||
route: 'hasura',
|
||||
slug: 'hasura',
|
||||
},
|
||||
{
|
||||
name: 'Auth',
|
||||
icon: <UserIcon className="w-4 h-4" />,
|
||||
icon: <UserIcon className="h-4 w-4" />,
|
||||
route: 'users',
|
||||
slug: 'users',
|
||||
},
|
||||
{
|
||||
name: 'Storage',
|
||||
icon: <StorageIcon className="w-4 h-4" />,
|
||||
icon: <StorageIcon className="h-4 w-4" />,
|
||||
route: 'storage',
|
||||
slug: 'storage',
|
||||
},
|
||||
{
|
||||
name: 'Run',
|
||||
icon: <ServicesIcon className="w-4 h-4" />,
|
||||
icon: <ServicesIcon className="h-4 w-4" />,
|
||||
route: 'services',
|
||||
slug: 'services',
|
||||
},
|
||||
{
|
||||
name: 'AI',
|
||||
icon: <AIIcon className="w-4 h-4" />,
|
||||
icon: <AIIcon className="h-4 w-4" />,
|
||||
route: 'ai/auto-embeddings',
|
||||
slug: 'ai',
|
||||
},
|
||||
{
|
||||
name: 'Deployments',
|
||||
icon: <RocketIcon className="w-4 h-4" />,
|
||||
icon: <RocketIcon className="h-4 w-4" />,
|
||||
route: 'deployments',
|
||||
slug: 'deployments',
|
||||
},
|
||||
{
|
||||
name: 'Backups',
|
||||
icon: <CloudIcon className="w-4 h-4" />,
|
||||
icon: <CloudIcon className="h-4 w-4" />,
|
||||
route: 'backups',
|
||||
slug: 'backups',
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
icon: <FileTextIcon className="w-4 h-4" />,
|
||||
icon: <FileTextIcon className="h-4 w-4" />,
|
||||
route: 'logs',
|
||||
slug: 'logs',
|
||||
},
|
||||
{
|
||||
name: 'Metrics',
|
||||
icon: <GaugeIcon className="w-4 h-4" />,
|
||||
icon: <GaugeIcon className="h-4 w-4" />,
|
||||
route: 'metrics',
|
||||
slug: 'metrics',
|
||||
},
|
||||
@@ -210,7 +210,7 @@ const createWorkspace = (workspace: Workspace) => {
|
||||
data: {
|
||||
name: app.name,
|
||||
slug: app.slug,
|
||||
icon: <Box className="w-4 h-4" />,
|
||||
icon: <Box className="h-4 w-4" />,
|
||||
targetUrl: `/${workspace.slug}/${app.slug}`,
|
||||
},
|
||||
children: projectPages.map(
|
||||
@@ -324,7 +324,7 @@ export default function WorkspacesNavTree() {
|
||||
className={cn(
|
||||
item?.index === 'workspaces' && 'font-bold',
|
||||
context.isFocused ? 'font-bold text-primary' : '',
|
||||
'max-w-52 truncate',
|
||||
'max-w-40 truncate',
|
||||
)}
|
||||
>
|
||||
{item.data.name}
|
||||
@@ -350,7 +350,7 @@ export default function WorkspacesNavTree() {
|
||||
className="font-medium"
|
||||
>
|
||||
announcement
|
||||
<ArrowSquareOutIcon className="w-4 h-4 mb-1 ml-1" />
|
||||
<ArrowSquareOutIcon className="mb-1 ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
@@ -421,9 +421,9 @@ export default function WorkspacesNavTree() {
|
||||
className="h-8 px-1"
|
||||
>
|
||||
{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>
|
||||
);
|
||||
@@ -444,9 +444,9 @@ export default function WorkspacesNavTree() {
|
||||
}
|
||||
|
||||
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="w-0 h-full border-r border-dashed" />
|
||||
<div className="h-full w-0 border-r border-dashed" />
|
||||
</div>
|
||||
<ul {...containerProps} className="w-full">
|
||||
{children}
|
||||
|
||||
@@ -17,18 +17,18 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
return (
|
||||
<Box
|
||||
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
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex h-full flex-col"
|
||||
className="flex flex-col h-full"
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{hasGitRepo && (
|
||||
<Alert
|
||||
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">
|
||||
As you have a connected repository, make sure to synchronize
|
||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>{' '}
|
||||
for guidance.
|
||||
</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 };
|
||||
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/30 py-4 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'fixed inset-0 z-[50] grid place-items-center overflow-y-auto bg-black/30 py-4 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -3,20 +3,30 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
prefix?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className, type, prefix, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
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',
|
||||
className,
|
||||
<div className="relative flex items-center">
|
||||
{prefix && (
|
||||
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<input
|
||||
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 { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -30,7 +29,6 @@ export default function DisableNewUsersSettings() {
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -42,14 +40,14 @@ export default function DisableNewUsersSettings() {
|
||||
const form = useForm<DisableNewUsersFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
});
|
||||
}
|
||||
}, [loading, data, form]);
|
||||
@@ -58,7 +56,7 @@ export default function DisableNewUsersSettings() {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading disabled sign up settings..."
|
||||
label="Loading disabled new users settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
@@ -79,7 +77,7 @@ export default function DisableNewUsersSettings() {
|
||||
config: {
|
||||
auth: {
|
||||
signUp: {
|
||||
enabled: !values.disabled,
|
||||
disableNewUsers: values.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -104,10 +102,10 @@ export default function DisableNewUsersSettings() {
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Disabling new user sign ups...',
|
||||
successMessage: 'New user sign ups have been disabled successfully.',
|
||||
loadingMessage: 'Disabling new users sign ins...',
|
||||
successMessage: 'New users sign ins have been disabled successfully.',
|
||||
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 {
|
||||
enabled
|
||||
disableNewUsers
|
||||
}
|
||||
session {
|
||||
accessToken {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
|
||||
import { StripeEmbeddedForm } from '@/features/orgs/components/StripeEmbeddedForm';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
@@ -98,13 +100,13 @@ function CreateOrgForm({ plans, onSubmit, onCancel }: CreateOrgFormProps) {
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FormItem key={plan.id}>
|
||||
<FormLabel className="flex flex-row items-center justify-between w-full p-3 space-y-0 border rounded-md cursor-pointer">
|
||||
<FormLabel className="flex w-full cursor-pointer flex-row items-center justify-between space-y-0 rounded-md border p-3">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
<FormControl>
|
||||
<RadioGroupItem value={plan.id} />
|
||||
</FormControl>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="font-semibold text-md">
|
||||
<div className="text-md font-semibold">
|
||||
{plan.name}
|
||||
</div>
|
||||
<FormDescription className="w-2/3 text-xs">
|
||||
@@ -113,12 +115,37 @@ function CreateOrgForm({ plans, onSubmit, onCancel }: CreateOrgFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full mt-0 text-xl font-semibold">
|
||||
{plan.isFree ? 'Free' : `${plan.price}/mo`}
|
||||
<div className="mt-0 flex h-full items-center text-xl font-semibold">
|
||||
{plan.isFree ? 'Free' : `$${plan.price}/mo`}
|
||||
</div>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
))}
|
||||
<div>
|
||||
<div className="flex w-full cursor-pointer flex-row items-center justify-between space-y-0 rounded-md border p-3">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="text-md font-semibold">
|
||||
Enterprise
|
||||
</div>
|
||||
<div className="w-2/3 text-xs">
|
||||
{planDescriptions.Enterprise}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="mailto:hello@nhost.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
className="font-medium"
|
||||
>
|
||||
Contact us
|
||||
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -207,7 +234,7 @@ export default function CreateOrgDialog() {
|
||||
)}
|
||||
onClick={() => setStripeClientSecret('')}
|
||||
>
|
||||
<Plus className="w-4 h-4 font-bold" strokeWidth={3} />
|
||||
<Plus className="h-4 w-4 font-bold" strokeWidth={3} />
|
||||
New Organization
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -225,7 +252,7 @@ export default function CreateOrgDialog() {
|
||||
</DialogHeader>
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center h-52">
|
||||
<div className="flex h-52 items-center justify-center">
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{
|
||||
className: 'w-5 h-5',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function TransferProject() {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -18,7 +20,7 @@ export default function TransferProject() {
|
||||
type: 'button',
|
||||
color: 'primary',
|
||||
variant: 'contained',
|
||||
disabled: maintenanceActive,
|
||||
disabled: maintenanceActive || !isPlatform,
|
||||
onClick: () => setOpen(true),
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
@@ -44,7 +45,7 @@ export default function SubscriptionPlan() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [changeOrgPlan] = useBillingChangeOrganizationPlanMutation();
|
||||
const { data: { plans = [] } = {} } = useGetOrganizationPlansQuery();
|
||||
const [fetchOrganizationCustomePortalLink] =
|
||||
const [fetchOrganizationCustomePortalLink, { loading }] =
|
||||
useBillingOrganizationCustomePortalLazyQuery();
|
||||
|
||||
const form = useForm<z.infer<typeof changeOrgPlanForm>>({
|
||||
@@ -100,7 +101,10 @@ export default function SubscriptionPlan() {
|
||||
});
|
||||
|
||||
if (billingOrganizationCustomePortal) {
|
||||
window.open(billingOrganizationCustomePortal);
|
||||
const newWindow = window.open(billingOrganizationCustomePortal);
|
||||
if (!newWindow) {
|
||||
window.location.href = billingOrganizationCustomePortal;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not fetch customer portal link');
|
||||
}
|
||||
@@ -117,35 +121,39 @@ export default function SubscriptionPlan() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-col w-full border rounded-md bg-background">
|
||||
<div className="flex flex-col w-full gap-1 p-4 border-b">
|
||||
<div className="flex w-full flex-col rounded-md border bg-background">
|
||||
<div className="flex w-full flex-col gap-1 border-b p-4">
|
||||
<h4 className="font-medium">Subscription plan</h4>
|
||||
</div>
|
||||
<div className="flex flex-col border-b md:flex-row">
|
||||
<div className="flex flex-col w-full gap-4 p-4">
|
||||
<div className="flex w-full flex-col justify-between gap-8 border-b p-4 md:flex-row">
|
||||
<div className="flex basis-1/2 flex-col gap-4">
|
||||
<span className="font-medium">Organization name</span>
|
||||
<span className="font-medium">{org?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-2 p-4">
|
||||
<span className="font-medium">Current plan</span>
|
||||
<span className="text-xl font-bold text-primary-main">
|
||||
{org?.plan?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start w-full 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}
|
||||
<div className="flex flex-1 flex-col gap-8 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span className="font-medium">Current plan</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
{org?.plan?.name}
|
||||
</span>
|
||||
<Slash
|
||||
className="w-5 h-5 text-muted-foreground/40"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span className="text-xl font-semibold">month</span>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
${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 className="flex flex-col-reverse items-end justify-between w-full 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>
|
||||
<span>For a complete list of features, visit our </span>
|
||||
<Link
|
||||
@@ -156,17 +164,21 @@ export default function SubscriptionPlan() {
|
||||
className="font-medium"
|
||||
>
|
||||
pricing
|
||||
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
|
||||
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</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
|
||||
className="h-fit"
|
||||
className="h-fit truncate"
|
||||
variant="secondary"
|
||||
onClick={handleUpdatePaymentDetails}
|
||||
disabled={org?.plan?.isFree || maintenanceActive}
|
||||
disabled={org?.plan?.isFree || maintenanceActive || loading}
|
||||
>
|
||||
Stripe Customer Portal
|
||||
{loading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<span className="truncate">Stripe Customer Portal</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={org?.plan?.isFree || maintenanceActive}
|
||||
@@ -205,13 +217,13 @@ export default function SubscriptionPlan() {
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FormItem key={plan.id}>
|
||||
<FormLabel className="flex flex-row items-center justify-between w-full p-3 space-y-0 border rounded-md cursor-pointer">
|
||||
<FormLabel className="flex w-full cursor-pointer flex-row items-center justify-between space-y-0 rounded-md border p-3">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
<FormControl>
|
||||
<RadioGroupItem value={plan.id} />
|
||||
</FormControl>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="font-semibold text-md">
|
||||
<div className="text-md font-semibold">
|
||||
{plan.name}
|
||||
</div>
|
||||
<FormDescription className="w-2/3 text-xs">
|
||||
@@ -220,7 +232,7 @@ export default function SubscriptionPlan() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full mt-0 text-xl font-semibold">
|
||||
<div className="mt-0 flex h-full items-center text-xl font-semibold">
|
||||
{plan.isFree ? 'Free' : `${plan.price}/mo`}
|
||||
</div>
|
||||
</FormLabel>
|
||||
@@ -228,10 +240,10 @@ export default function SubscriptionPlan() {
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-between w-full p-3 space-y-0 border rounded-md cursor-pointer">
|
||||
<div className="flex w-full cursor-pointer flex-row items-center justify-between space-y-0 rounded-md border p-3">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="font-semibold text-md">
|
||||
<div className="text-md font-semibold">
|
||||
Enterprise
|
||||
</div>
|
||||
<div className="w-2/3 text-xs">
|
||||
@@ -248,7 +260,7 @@ export default function SubscriptionPlan() {
|
||||
className="font-medium"
|
||||
>
|
||||
Contact us
|
||||
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
|
||||
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
const progress = (daysPassed / totalDays) * 100;
|
||||
const daysLeft = Math.max(Math.ceil(totalDays - daysPassed), 0);
|
||||
|
||||
return {
|
||||
billingCycleStart,
|
||||
billingCycleEnd,
|
||||
billingCycleRange: `${billingCycleStart} - ${billingCycleEnd}`,
|
||||
progress: Math.min(Math.max(progress, 0), 100), // Ensure the value is between 0 and 100
|
||||
daysLeft,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,13 +24,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/v3/select';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useBillingTransferAppMutation } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
Organization_Members_Role_Enum,
|
||||
useBillingTransferAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useUserId } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
@@ -48,10 +51,10 @@ export default function TransferProjectDialog({
|
||||
open,
|
||||
setOpen,
|
||||
}: TransferProjectDialogProps) {
|
||||
const { orgs } = useOrgs();
|
||||
const { org: currentOrg } = useCurrentOrg();
|
||||
const { project } = useProject();
|
||||
const { push } = useRouter();
|
||||
const currentUserId = useUserId();
|
||||
const { project } = useProject();
|
||||
const { orgs, currentOrg } = useOrgs();
|
||||
const [transferProject] = useBillingTransferAppMutation();
|
||||
|
||||
const form = useForm<z.infer<typeof transferProjectFormSchema>>({
|
||||
@@ -86,16 +89,29 @@ export default function TransferProjectDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const isUserAdminOfOrg = (org: Org, userId: string) =>
|
||||
org.members.some(
|
||||
(member) =>
|
||||
member.role === Organization_Members_Role_Enum.Admin &&
|
||||
member.user.id === userId,
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="text-foreground sm:max-w-xl">
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(value) => {
|
||||
form.reset();
|
||||
setOpen(value);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="z-[9999] text-foreground sm:max-w-xl">
|
||||
<DialogHeader className="flex gap-2">
|
||||
<DialogTitle>
|
||||
Move the current project to a different organization.
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
To transfer a project between two organizations, you must be ADMIN
|
||||
in both.
|
||||
To transfer a project between organizations, you must be an{' '}
|
||||
<span className="font-bold">ADMIN</span> in both.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
@@ -120,7 +136,11 @@ export default function TransferProjectDialog({
|
||||
<SelectItem
|
||||
key={org.id}
|
||||
value={org.id}
|
||||
disabled={org.plan.isFree || org.id === currentOrg.id}
|
||||
disabled={
|
||||
org.plan.isFree || // disable the personal org
|
||||
org.id === currentOrg.id || // disable the current org as it can't be a destination org
|
||||
!isUserAdminOfOrg(org, currentUserId) // disable orgs that the current user is not admin of
|
||||
}
|
||||
>
|
||||
{org.name}
|
||||
<Badge
|
||||
@@ -146,11 +166,19 @@ export default function TransferProjectDialog({
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={form.formState.isSubmitting}
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
form.formState.isSubmitting || !form.formState.isDirty
|
||||
}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
|
||||
@@ -34,9 +34,15 @@ import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function NotificationsTray() {
|
||||
const { refetch: refetchOrgs } = useOrgs();
|
||||
const { asPath, route } = useRouter();
|
||||
const userData = useUserData();
|
||||
const { asPath, route } = useRouter();
|
||||
const { refetch: refetchOrgs } = useOrgs();
|
||||
|
||||
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
|
||||
|
||||
const [pendingOrgRequest, setPendingOrgRequest] =
|
||||
useState<PostOrganizationRequestResponse | null>(null);
|
||||
|
||||
const [
|
||||
getInvites,
|
||||
{
|
||||
@@ -45,11 +51,6 @@ export default function NotificationsTray() {
|
||||
data: { organizationMemberInvites: invites = [] } = {},
|
||||
},
|
||||
] = useOrganizationMemberInvitesLazyQuery();
|
||||
|
||||
const [stripeFormDialogOpen, setStripeFormDialogOpen] = useState(false);
|
||||
|
||||
const [pendingOrgRequest, setPendingOrgRequest] =
|
||||
useState<PostOrganizationRequestResponse | null>(null);
|
||||
const [getOrganizationNewRequests] = useOrganizationNewRequestsLazyQuery();
|
||||
const [postOrganizationRequest] = usePostOrganizationRequestMutation();
|
||||
|
||||
@@ -155,12 +156,11 @@ export default function NotificationsTray() {
|
||||
<>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" className="relative px-3 py-1 h-fit">
|
||||
<Button variant="ghost" className="relative h-fit px-3 py-1">
|
||||
<Bell className="mt-[2px] h-[1.15rem] w-[1.15rem]" />
|
||||
{invites.length > 0 ||
|
||||
(pendingOrgRequest && (
|
||||
<div className="absolute w-2 h-2 bg-red-500 rounded-full right-3 top-2" />
|
||||
))}
|
||||
{(pendingOrgRequest || Boolean(invites.length)) && (
|
||||
<div className="absolute right-3 top-2 h-2 w-2 rounded-full bg-red-500" />
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="h-full w-full bg-background p-0 text-foreground sm:max-w-[310px]">
|
||||
@@ -170,14 +170,14 @@ export default function NotificationsTray() {
|
||||
List of pending invites and create organization requests
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<div className="flex items-center h-12 px-2 border-b">
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-12 items-center border-b px-2">
|
||||
<h3 className="font-medium">
|
||||
Notifications {invites.length > 0 && `(${invites.length})`}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
<div className="flex h-full flex-col gap-2 overflow-auto p-2">
|
||||
{!loading && invites.length === 0 && !pendingOrgRequest && (
|
||||
<span className="text-muted-foreground">
|
||||
No new notifications
|
||||
@@ -185,9 +185,9 @@ export default function NotificationsTray() {
|
||||
)}
|
||||
|
||||
{pendingOrgRequest && (
|
||||
<div className="flex flex-col gap-2 p-2 border rounded-md">
|
||||
<div className="flex flex-col gap-2 rounded-md border p-2">
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Badge className="h-5 px-[6px] text-[10px]">
|
||||
New Organization pending
|
||||
</Badge>
|
||||
@@ -212,10 +212,10 @@ export default function NotificationsTray() {
|
||||
{invites.map((invite) => (
|
||||
<div
|
||||
key={invite.id}
|
||||
className="flex flex-col gap-2 p-2 border rounded-md"
|
||||
className="flex flex-col gap-2 rounded-md border p-2"
|
||||
>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Badge className="h-5 px-[6px] text-[10px]">
|
||||
Invitation
|
||||
</Badge>
|
||||
@@ -265,7 +265,7 @@ export default function NotificationsTray() {
|
||||
onOpenChange={setStripeFormDialogOpen}
|
||||
>
|
||||
<DialogContent
|
||||
className="text-black bg-white sm:max-w-xl"
|
||||
className="bg-white text-black sm:max-w-xl"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -25,9 +25,9 @@ function ProjectCard({ project }: { project: Project }) {
|
||||
className="flex cursor-pointer flex-col gap-4 rounded-lg border bg-background p-4 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Box className="h-6 w-6" />
|
||||
<h2 className="text-lg font-semibold">{project.name}</h2>
|
||||
<div className="flex w-full flex-row items-center space-x-2">
|
||||
<Box className="h-6 w-6 flex-shrink-0" />
|
||||
<p className="truncate text-lg font-bold">{project.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function ErrorToast({
|
||||
style={{
|
||||
backgroundColor: getToastBackgroundColor(),
|
||||
}}
|
||||
className="flex w-full max-w-xl flex-col space-y-4 rounded-lg p-4 text-white"
|
||||
className="flex flex-col w-full max-w-xl p-4 space-y-4 text-white rounded-lg"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100,
|
||||
@@ -112,24 +112,30 @@ export default function ErrorToast({
|
||||
bounce: 0.1,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between space-x-4">
|
||||
<button onClick={close} type="button" aria-label="Close">
|
||||
<XIcon className="h-4 w-4 text-white" />
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<button
|
||||
className="flex-shrink-0"
|
||||
onClick={close}
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XIcon className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
<span>
|
||||
<span className="flex-grow overflow-hidden break-words">
|
||||
{msg ?? 'An unkown error has occured, please try again later!'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
className="flex flex-row items-center justify-center space-x-2 text-white"
|
||||
className="flex flex-row items-center justify-center flex-shrink-0 space-x-2 text-white"
|
||||
aria-label="Show error details"
|
||||
>
|
||||
<span>Info</span>
|
||||
{showInfo ? (
|
||||
<ChevronUpIcon className="h-3 w-3 text-white" />
|
||||
<ChevronUpIcon className="w-3 h-3 text-white" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-3 w-3 text-white" />
|
||||
<ChevronDownIcon className="w-3 h-3 text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -152,7 +158,7 @@ export default function ErrorToast({
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex flex-col flex-auto w-full overflow-x-hidden overflow-y-auto"
|
||||
className="flex flex-col flex-auto w-full h-full overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { MessagesList } from '@/features/ai/DevAssistant/components/MessagesList';
|
||||
import { MessagesList } from '@/features/orgs/projects/ai/DevAssistant/components/MessagesList';
|
||||
import {
|
||||
messagesState,
|
||||
projectMessagesState,
|
||||
sessionIDState,
|
||||
} from '@/features/ai/DevAssistant/state';
|
||||
import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsGraphiteEnabled } from '@/features/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
} from '@/features/orgs/projects/ai/DevAssistant/state';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
useSendDevMessageMutation,
|
||||
useStartDevSessionMutation,
|
||||
@@ -36,12 +37,13 @@ export type Message = Omit<
|
||||
|
||||
export default function DevAssistant() {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentProject, currentWorkspace } = useCurrentWorkspaceAndProject();
|
||||
const { project } = useProject();
|
||||
const { currentOrg } = useOrgs();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userInput, setUserInput] = useState('');
|
||||
const setMessages = useSetRecoilState(messagesState);
|
||||
const messages = useRecoilValue(projectMessagesState(currentProject.id));
|
||||
const messages = useRecoilValue(projectMessagesState(project?.id));
|
||||
const [storedSessionID, setStoredSessionID] = useRecoilState(sessionIDState);
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
@@ -74,7 +76,7 @@ export default function DevAssistant() {
|
||||
message: userInput,
|
||||
createdAt: null,
|
||||
role: 'user',
|
||||
projectId: currentProject.id,
|
||||
projectId: project?.id,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -113,7 +115,7 @@ export default function DevAssistant() {
|
||||
.filter((item) => item.message)
|
||||
|
||||
// add the currentProject.id to the new messages
|
||||
.map((item) => ({ ...item, projectId: currentProject.id })),
|
||||
.map((item) => ({ ...item, projectId: project?.id })),
|
||||
];
|
||||
|
||||
if (thread.length > MAX_THREAD_LENGTH) {
|
||||
@@ -152,36 +154,29 @@ export default function DevAssistant() {
|
||||
}
|
||||
};
|
||||
|
||||
if (isPlatform && currentProject?.legacyPlan?.isFree) {
|
||||
if (isPlatform && currentOrg?.plan?.isFree) {
|
||||
return (
|
||||
<Box className="p-4">
|
||||
<UpgradeToProBanner
|
||||
title="Upgrade to Nhost Pro."
|
||||
description={
|
||||
<Text>
|
||||
Graphite is an addon to the Pro plan. To unlock it, please upgrade
|
||||
to Pro first.
|
||||
</Text>
|
||||
}
|
||||
title="To unlock the DevAssistant, transfer this project to a Pro or Team organization."
|
||||
description=""
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(isPlatform &&
|
||||
!currentProject?.legacyPlan?.isFree &&
|
||||
!currentProject?.config?.ai) ||
|
||||
(isPlatform && !currentOrg?.plan?.isFree && !project?.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
) {
|
||||
return (
|
||||
<Box className="p-4">
|
||||
<Alert className="grid items-center w-full grid-flow-col gap-2 place-content-between">
|
||||
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
|
||||
<Text className="grid grid-flow-row justify-items-start gap-0.5">
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/${currentWorkspace?.slug}/${currentProject?.slug}/settings/ai`}
|
||||
href={`/orgs/${currentOrg?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
@@ -197,11 +192,11 @@ export default function DevAssistant() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto">
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<MessagesList loading={loading} />
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Box className="relative flex flex-row justify-between w-full p-2">
|
||||
<Box className="relative flex w-full flex-row justify-between p-2">
|
||||
<Input
|
||||
value={userInput}
|
||||
onChange={(event) => {
|
||||
@@ -224,7 +219,7 @@ export default function DevAssistant() {
|
||||
color="primary"
|
||||
aria-label="Send"
|
||||
type="submit"
|
||||
className="absolute self-end w-12 h-10 right-2 rounded-xl"
|
||||
className="absolute right-2 h-10 w-12 self-end rounded-xl"
|
||||
>
|
||||
{loading ? <ActivityIndicator /> : <ArrowUpIcon />}
|
||||
</IconButton>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { type Message } from '@/features/ai/DevAssistant';
|
||||
import { type Message } from '@/features/orgs/projects/ai/DevAssistant';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
@@ -24,7 +24,7 @@ function PreComponent(
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<div className="group relative">
|
||||
<pre>{children}</pre>
|
||||
<IconButton
|
||||
sx={{
|
||||
@@ -34,13 +34,13 @@ function PreComponent(
|
||||
}}
|
||||
color="warning"
|
||||
variant="contained"
|
||||
className="absolute hidden top-2 right-2 group-hover:flex"
|
||||
className="absolute right-2 top-2 hidden group-hover:flex"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(onlyText(children), 'Snippet');
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-5 h-5" />
|
||||
<CopyIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
@@ -53,7 +53,7 @@ export default function MessageBox({ message }: { message: Message }) {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="flex flex-col p-4 space-y-4 border-t first:border-t-0"
|
||||
className="flex flex-col space-y-4 border-t p-4 first:border-t-0"
|
||||
sx={{
|
||||
backgroundColor: isUserMessage && 'background.default',
|
||||
}}
|
||||
@@ -67,7 +67,7 @@ export default function MessageBox({ message }: { message: Message }) {
|
||||
) : (
|
||||
<>
|
||||
<Avatar
|
||||
className="rounded-full h-7 w-7"
|
||||
className="h-7 w-7 rounded-full"
|
||||
alt={user?.displayName}
|
||||
src={user?.avatarUrl}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { LoadingAssistantMessage } from '@/features/ai/DevAssistant/components/LoadingAssistantMessage';
|
||||
import { MessageBox } from '@/features/ai/DevAssistant/components/MessageBox';
|
||||
import { projectMessagesState } from '@/features/ai/DevAssistant/state';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { LoadingAssistantMessage } from '@/features/orgs/projects/ai/DevAssistant/components/LoadingAssistantMessage';
|
||||
import { projectMessagesState } from '@/features/orgs/projects/ai/DevAssistant/state';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
@@ -11,9 +11,9 @@ interface MessagesListProps {
|
||||
}
|
||||
|
||||
function MessagesList({ loading }: MessagesListProps) {
|
||||
const { project } = useProject();
|
||||
const bottomElement = useRef(null);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const messages = useRecoilValue(projectMessagesState(currentProject.id));
|
||||
const messages = useRecoilValue(projectMessagesState(project?.id));
|
||||
|
||||
const scrollToBottom = () =>
|
||||
bottomElement?.current?.scrollIntoView({ behavior: 'instant' });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
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 { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
@@ -26,11 +26,12 @@ export default function DisableAIServiceConfirmationDialog({
|
||||
onCancel,
|
||||
onServiceDisabled,
|
||||
}: DisableAIServiceConfirmationDialogProps) {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
@@ -42,7 +43,7 @@ export default function DisableAIServiceConfirmationDialog({
|
||||
async () => {
|
||||
await updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
ai: null,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -32,13 +31,13 @@ export type AllowedEmailSettingsFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AllowedEmailDomainsSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -29,13 +28,13 @@ export type AllowedRedirectURLFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AllowedRedirectURLsSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
||||
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AnonymousSignInSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -58,13 +57,13 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AppleProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -269,7 +268,7 @@ export default function AppleProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
Software_Type_Enum,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useGetSoftwareVersionsQuery,
|
||||
@@ -34,13 +33,13 @@ export type AuthServiceVersionFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AuthServiceVersionSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -154,7 +153,7 @@ export default function AuthServiceVersionSettings() {
|
||||
}}
|
||||
docsLink="https://github.com/nhost/hasura-auth/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
|
||||
id="version"
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -51,13 +50,13 @@ const validationSchema = Yup.object({
|
||||
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AzureADProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -214,7 +213,7 @@ export default function AzureADProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -30,13 +29,13 @@ const validationSchema = Yup.object({
|
||||
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function BlockedEmailSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -27,13 +26,13 @@ const validationSchema = Yup.object({
|
||||
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function ClientURLSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -27,13 +26,13 @@ export type ToggleConcealErrorsFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function ConcealErrorsSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
@@ -52,16 +52,24 @@ function ConfirmDeleteSMTPSettingsModal({
|
||||
}
|
||||
|
||||
export default function DeleteSMTPSettings() {
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
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({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -111,7 +119,10 @@ export default function DeleteSMTPSettings() {
|
||||
component: (
|
||||
<ConfirmDeleteSMTPSettingsModal
|
||||
close={closeDialog}
|
||||
onDelete={deleteSMTPSettings}
|
||||
onDelete={async () => {
|
||||
await deleteSMTPSettings();
|
||||
await refetch();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -132,7 +143,7 @@ export default function DeleteSMTPSettings() {
|
||||
color="error"
|
||||
className="mx-4 mt-4 justify-self-end"
|
||||
onClick={confirmDeleteSMTPSettings}
|
||||
disabled={loading || maintenanceActive}
|
||||
disabled={loading || maintenanceActive || !isSMTPConfigured}
|
||||
loading={loading}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
||||
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function DisableNewUsersSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -43,14 +42,14 @@ export default function DisableNewUsersSettings() {
|
||||
const form = useForm<DisableNewUsersFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
disabled: !data?.config?.auth?.signUp?.enabled,
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
});
|
||||
}
|
||||
}, [loading, data, form]);
|
||||
@@ -59,7 +58,7 @@ export default function DisableNewUsersSettings() {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading disabled sign up settings..."
|
||||
label="Loading disabled new users settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
@@ -80,7 +79,7 @@ export default function DisableNewUsersSettings() {
|
||||
config: {
|
||||
auth: {
|
||||
signUp: {
|
||||
enabled: !values.disabled,
|
||||
disableNewUsers: values.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -105,10 +104,10 @@ export default function DisableNewUsersSettings() {
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Disabling new user sign ups...',
|
||||
successMessage: 'New user sign ups have been disabled successfully.',
|
||||
loadingMessage: 'Disabling new users sign ins...',
|
||||
successMessage: 'New users sign ins have been disabled successfully.',
|
||||
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 { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -30,13 +29,13 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function DiscordProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -184,7 +183,7 @@ export default function DiscordProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
||||
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function EmailAndPasswordSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function FacebookProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function FacebookProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
||||
|
||||
export default function GitHubProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function GitHubProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function GoogleProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function GoogleProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
||||
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function GravatarSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function LinkedInProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function LinkedInProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -29,18 +28,19 @@ const validationSchema = Yup.object({
|
||||
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function MFASettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
||||
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function MagicLinkSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -28,21 +27,21 @@ const validationSchema = yup
|
||||
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function PostmarkSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { sender, password } = data?.config?.provider?.smtp || {};
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -81,6 +80,8 @@ export default function PostmarkSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset({ ...values });
|
||||
await refetch();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -50,17 +49,17 @@ const validationSchema = Yup.object({
|
||||
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SMSSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, error, loading } = useGetSignInMethodsQuery({
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -38,14 +37,15 @@ const smtpValidationSchema = yup
|
||||
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||
|
||||
export default function SMTPSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -83,7 +83,6 @@ export default function SMTPSettings() {
|
||||
} = form;
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -104,6 +103,8 @@ export default function SMTPSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset({ ...values });
|
||||
await refetch();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -34,13 +33,13 @@ const validationSchema = Yup.object({
|
||||
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SessionSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function SpotifyProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function SpotifyProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
||||
|
||||
export default function TwitchProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function TwitchProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -45,13 +44,13 @@ const validationSchema = Yup.object({
|
||||
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function TwitterProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -217,7 +216,7 @@ export default function TwitterProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
||||
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function WebAuthnSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function WindowsLiveProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -183,7 +182,7 @@ export default function WindowsLiveProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -57,13 +56,13 @@ const validationSchema = Yup.object({
|
||||
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function WorkOsProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -244,7 +243,7 @@ export default function WorkOsProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ query GetAuthenticationSettings($appId: uuid!) {
|
||||
}
|
||||
signUp {
|
||||
enabled
|
||||
disableNewUsers
|
||||
}
|
||||
session {
|
||||
accessToken {
|
||||
|
||||
@@ -2,11 +2,9 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useDeleteApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useBillingDeleteAppMutation } from '@/generated/graphql';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
@@ -14,17 +12,22 @@ import { formatDistance } from 'date-fns';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function ApplicationInfo() {
|
||||
const { project } = useProject();
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const router = useRouter();
|
||||
const { project } = useProject();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
|
||||
const [deleteApplication] = useBillingDeleteAppMutation();
|
||||
|
||||
async function handleClickRemove() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await deleteApplication({ variables: { appId: project.id } });
|
||||
await router.push('/');
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appID: project?.id,
|
||||
},
|
||||
});
|
||||
|
||||
await router.push(`/orgs/${org?.slug}/projects`);
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Deleting project...',
|
||||
@@ -40,7 +43,7 @@ export default function ApplicationInfo() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-4 mt-4">
|
||||
<div className="mt-4 grid grid-flow-row gap-4">
|
||||
<div className="grid grid-flow-row justify-center gap-0.5">
|
||||
<Text variant="subtitle2">Application ID:</Text>
|
||||
|
||||
@@ -88,7 +91,7 @@ export default function ApplicationInfo() {
|
||||
href={`https://staging.nhost.run/console/data/default/schema/public/tables/app_state_history/browse?filter=app_id%3B%24eq%3B${project.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="grid items-center justify-center grid-flow-col gap-1 p-2"
|
||||
className="grid grid-flow-col items-center justify-center gap-1 p-2"
|
||||
underline="hover"
|
||||
>
|
||||
App State History <ArrowRightIcon />
|
||||
|
||||
@@ -42,6 +42,9 @@ export default function ApplicationPaused() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await unpauseApplication({ variables: { appId: project.id } });
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
await refetchProject();
|
||||
},
|
||||
{
|
||||
@@ -62,17 +65,19 @@ export default function ApplicationPaused() {
|
||||
<Modal
|
||||
showModal={showDeletingModal}
|
||||
close={() => setShowDeletingModal(false)}
|
||||
className="flex h-screen items-center justify-center"
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
close={() => setShowDeletingModal(false)}
|
||||
title={`Remove project ${project.name}?`}
|
||||
description={`The project ${project.name} will be removed. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
className="z-50"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Container className="grid max-w-lg grid-flow-row gap-6 mx-auto text-center">
|
||||
<div className="flex flex-col mx-auto text-center w-centImage">
|
||||
<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">
|
||||
<ApplicationPausedSymbol isLocked={isLocked} />
|
||||
</div>
|
||||
|
||||
@@ -93,7 +98,7 @@ export default function ApplicationPaused() {
|
||||
{org && (
|
||||
<>
|
||||
<Button
|
||||
className="w-full max-w-xs mx-auto"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setTransferProjectDialogOpen(true)}
|
||||
>
|
||||
Transfer
|
||||
@@ -106,7 +111,7 @@ export default function ApplicationPaused() {
|
||||
)}
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="w-full max-w-xs mx-auto"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={
|
||||
changingApplicationStateLoading ||
|
||||
@@ -121,7 +126,7 @@ export default function ApplicationPaused() {
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
className="w-full max-w-xs mx-auto"
|
||||
className="mx-auto w-full max-w-xs"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ApplicationInfo } from '@/features/projects/common/components/ApplicationInfo';
|
||||
import { AppLoader } from '@/features/projects/common/components/AppLoader';
|
||||
import { StagingMetadata } from '@/features/projects/common/components/StagingMetadata';
|
||||
import { useCheckProvisioning } from '@/features/projects/common/hooks/useCheckProvisioning';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
|
||||
import { AppLoader } from '@/features/orgs/projects/common/components/AppLoader';
|
||||
import { StagingMetadata } from '@/features/orgs/projects/common/components/StagingMetadata';
|
||||
import { useCheckProvisioning } from '@/features/orgs/projects/common/hooks/useCheckProvisioning';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function ApplicationRestoring() {
|
||||
const { project } = useProject();
|
||||
const currentProjectState = useCheckProvisioning();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
return (
|
||||
<Container className="mx-auto mt-8 grid max-w-sm grid-flow-row gap-4 text-center">
|
||||
@@ -26,7 +26,7 @@ export default function ApplicationRestoring() {
|
||||
{currentProjectState.state === ApplicationStatus.Empty ? (
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h1">
|
||||
Setting Up {currentProject?.name}
|
||||
Setting Up {project?.name}
|
||||
</Text>
|
||||
|
||||
<Text>This normally takes around 2 minutes</Text>
|
||||
|
||||
@@ -3,13 +3,11 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useDeleteApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useBillingDeleteAppMutation } from '@/utils/__generated__/graphql';
|
||||
import router from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -44,14 +42,14 @@ export default function RemoveApplicationModal({
|
||||
description,
|
||||
className,
|
||||
}: RemoveApplicationModalProps) {
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
const { project } = useProject();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
const [deleteApplication] = useBillingDeleteAppMutation();
|
||||
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [remove2, setRemove2] = useState(false);
|
||||
|
||||
const appName = project?.name;
|
||||
|
||||
async function handleClick() {
|
||||
@@ -69,14 +67,14 @@ export default function RemoveApplicationModal({
|
||||
try {
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appID: project?.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await discordAnnounce(`Error trying to delete project: ${appName}`);
|
||||
}
|
||||
close();
|
||||
await router.push('/');
|
||||
await router.push(`/orgs/${org?.slug}/projects`);
|
||||
triggerToast(`${project.name} deleted`);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,9 +159,6 @@ export default function TOMLEditor() {
|
||||
height="100%"
|
||||
width="100%"
|
||||
theme={theme.palette.mode === 'light' ? bbedit : githubDark}
|
||||
basicSetup={{
|
||||
searchKeymap: false,
|
||||
}}
|
||||
extensions={[StreamLanguage.define(toml)]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -11,12 +11,12 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import type {
|
||||
ColumnType,
|
||||
DatabaseColumn,
|
||||
} from '@/features/database/dataGrid/types/dataBrowser';
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import {
|
||||
identityTypes,
|
||||
postgresFunctions,
|
||||
postgresTypeGroups,
|
||||
} from '@/features/database/dataGrid/utils/postgresqlConstants';
|
||||
} from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -102,7 +102,7 @@ export default function BaseColumnForm({
|
||||
return (
|
||||
<Form
|
||||
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">
|
||||
<section className="grid grid-cols-8 px-6 py-3">
|
||||
@@ -184,7 +184,7 @@ export default function BaseColumnForm({
|
||||
</Text>
|
||||
</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) => {
|
||||
if (checked) {
|
||||
setDefaultValueInputText('');
|
||||
@@ -197,7 +197,7 @@ export default function BaseColumnForm({
|
||||
|
||||
<Box
|
||||
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
|
||||
id="defaultValue"
|
||||
@@ -249,7 +249,7 @@ export default function BaseColumnForm({
|
||||
/>
|
||||
|
||||
<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"
|
||||
label={
|
||||
<span className="inline-grid grid-flow-row">
|
||||
@@ -269,7 +269,7 @@ export default function BaseColumnForm({
|
||||
/>
|
||||
|
||||
<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"
|
||||
label={
|
||||
<span className="inline-grid grid-flow-row">
|
||||
@@ -306,7 +306,7 @@ export default function BaseColumnForm({
|
||||
</Box>
|
||||
</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
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
|
||||
@@ -5,9 +5,9 @@ import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
||||
import { LinkIcon } from '@/components/ui/v2/icons/LinkIcon';
|
||||
import { InputLabel } from '@/components/ui/v2/InputLabel';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { CreateForeignKeyForm } from '@/features/database/dataGrid/components/CreateForeignKeyForm';
|
||||
import { EditForeignKeyForm } from '@/features/database/dataGrid/components/EditForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/features/database/dataGrid/types/dataBrowser';
|
||||
import { CreateForeignKeyForm } from '@/features/orgs/projects/database/dataGrid/components/CreateForeignKeyForm';
|
||||
import { EditForeignKeyForm } from '@/features/orgs/projects/database/dataGrid/components/EditForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useRef } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
@@ -106,7 +106,7 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
});
|
||||
}}
|
||||
variant="borderless"
|
||||
className="min-w-[initial] py-1 px-2"
|
||||
className="min-w-[initial] px-2 py-1"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -114,7 +114,7 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
<Button
|
||||
onClick={() => setValue('foreignKeyRelation', null)}
|
||||
variant="borderless"
|
||||
className="min-w-[initial] py-1 px-2"
|
||||
className="min-w-[initial] px-2 py-1"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useDatabaseQuery } from '@/features/database/dataGrid/hooks/useDatabaseQuery';
|
||||
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
|
||||
import type {
|
||||
DatabaseColumn,
|
||||
ForeignKeyRelation,
|
||||
} from '@/features/database/dataGrid/types/dataBrowser';
|
||||
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
@@ -107,9 +107,9 @@ export default function BaseForeignKeyForm({
|
||||
selectedColumn?.isPrimary || selectedColumn?.isUnique || false,
|
||||
});
|
||||
}}
|
||||
className="flex flex-col content-between flex-auto pb-4 overflow-hidden"
|
||||
className="flex flex-auto flex-col content-between overflow-hidden pb-4"
|
||||
>
|
||||
<Box className="grid flex-auto grid-flow-row gap-4 py-4 overflow-y-auto border-t-1">
|
||||
<Box className="grid flex-auto grid-flow-row gap-4 overflow-y-auto border-t-1 py-4">
|
||||
<Box component="section" className="grid grid-flow-row gap-4 px-6">
|
||||
<Text variant="h3">From</Text>
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function BaseForeignKeyForm({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="grid flex-shrink-0 grid-flow-row gap-2 px-6 pt-4 border-t-1">
|
||||
<Box className="grid flex-shrink-0 grid-flow-row gap-2 border-t-1 px-6 pt-4">
|
||||
<Button loading={isSubmitting} disabled={isSubmitting} type="submit">
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user