mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
2️⃣ Drop Personal organizations (#1452)
This commit is contained in:
parent
2ce7e73cac
commit
627df54206
35 changed files with 293 additions and 266 deletions
38
.github/workflows/migrations-test.yaml
vendored
Normal file
38
.github/workflows/migrations-test.yaml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14.6-alpine
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: registry
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
env:
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: registry
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: setup environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
codegen: false # no need to run codegen in this case, we can skip
|
||||
|
||||
- name: migrations tests
|
||||
run: pnpm test
|
||||
working-directory: packages/migrations
|
||||
4
.github/workflows/pr.yaml
vendored
4
.github/workflows/pr.yaml
vendored
|
|
@ -16,6 +16,10 @@ jobs:
|
|||
db-types:
|
||||
uses: ./.github/workflows/db-types-diff.yaml
|
||||
|
||||
# Run migrations tests
|
||||
db-migrations:
|
||||
uses: ./.github/workflows/migrations-test.yaml
|
||||
|
||||
# GraphQL Breaking Changes Check
|
||||
# This workflow validates that the GraphQL schema is not breaking, and fails in case of a breaking change.
|
||||
# To allow a GraphQL breaking change in a PR, you may add the "non-breaking" label to the PR.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ __generated__/
|
|||
/packages/web/app/src/gql/**
|
||||
/packages/web/app/src/graphql/index.ts
|
||||
/packages/web/app/next.config.mjs
|
||||
/packages/migrations/test/utils/testkit.ts
|
||||
|
||||
# test fixtures
|
||||
integration-tests/fixtures/init-invalid-schema.graphql
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const config = {
|
|||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: "import { StripeTypes } from '@hive/stripe-billing';",
|
||||
content: "import type { StripeTypes } from '@hive/stripe-billing';",
|
||||
},
|
||||
},
|
||||
'typescript',
|
||||
|
|
@ -26,7 +26,6 @@ const config = {
|
|||
immutableTypes: true,
|
||||
contextType: 'GraphQLModules.ModuleContext',
|
||||
enumValues: {
|
||||
OrganizationType: '../shared/entities#OrganizationType',
|
||||
ProjectType: '../shared/entities#ProjectType',
|
||||
TargetAccessScope: '../modules/auth/providers/target-access#TargetAccessScope',
|
||||
ProjectAccessScope: '../modules/auth/providers/project-access#ProjectAccessScope',
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ export function getOrganization(organizationId: string, authToken: string) {
|
|||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
getStarted {
|
||||
creatingProject
|
||||
publishingSchema
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
OrganizationAccessScope,
|
||||
OrganizationType,
|
||||
ProjectAccessScope,
|
||||
ProjectType,
|
||||
RegistryModel,
|
||||
|
|
@ -35,8 +34,6 @@ import {
|
|||
updateRegistryModel,
|
||||
updateSchemaVersionStatus,
|
||||
} from './flow';
|
||||
import { graphql } from './gql';
|
||||
import { execute } from './graphql';
|
||||
import { collect, CollectedOperation } from './usage';
|
||||
import { generateUnique } from './utils';
|
||||
|
||||
|
|
@ -51,48 +48,6 @@ export function initSeed() {
|
|||
return {
|
||||
ownerEmail,
|
||||
ownerToken,
|
||||
async createPersonalProject(projectType: ProjectType) {
|
||||
const orgs = await execute({
|
||||
document: graphql(`
|
||||
query myOrganizations {
|
||||
organizations {
|
||||
total
|
||||
nodes {
|
||||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
authToken: ownerToken,
|
||||
}).then(r => r.expectNoGraphQLErrors());
|
||||
|
||||
const personalOrg = orgs.organizations.nodes.find(
|
||||
o => o.type === OrganizationType.Personal,
|
||||
);
|
||||
|
||||
if (!personalOrg) {
|
||||
throw new Error('Personal organization should exist');
|
||||
}
|
||||
|
||||
const projectResult = await createProject(
|
||||
{
|
||||
organization: personalOrg.cleanId,
|
||||
type: projectType,
|
||||
name: generateUnique(),
|
||||
},
|
||||
ownerToken,
|
||||
).then(r => r.expectNoGraphQLErrors());
|
||||
|
||||
const targets = projectResult.createProject.ok!.createdTargets;
|
||||
const target = targets[0];
|
||||
|
||||
return {
|
||||
target,
|
||||
};
|
||||
},
|
||||
async createOrg() {
|
||||
const orgName = generateUnique();
|
||||
const orgResult = await createOrganization({ name: orgName }, ownerToken).then(r =>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
"db:migrator": "node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts",
|
||||
"migration:create": "pnpm db:migrator create",
|
||||
"migration:rollback": "pnpm db:migrator down",
|
||||
"migration:run": "pnpm db:migrator up"
|
||||
"migration:run": "pnpm db:migrator up",
|
||||
"test": "node --experimental-specifier-resolution=node --loader ts-node/esm ./test/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@slonik/migrator": "0.11.3",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
-- Find and delete all organizations of type PERSONAL that have no projects
|
||||
DELETE FROM public.organizations as o
|
||||
WHERE
|
||||
o.type = 'PERSONAL'
|
||||
AND NOT EXISTS (
|
||||
SELECT id from public.projects as p WHERE p.org_id = o.id LIMIT 1
|
||||
);
|
||||
|
||||
-- Delete the "type" column from organizations
|
||||
ALTER TABLE public.organizations DROP COLUMN type;
|
||||
|
||||
-- Delete the "organization_type" enum, as it's unused now
|
||||
DROP TYPE organization_type;
|
||||
|
|
@ -0,0 +1 @@
|
|||
raise 'down migration not implemented'
|
||||
11
packages/migrations/src/connection-string.ts
Normal file
11
packages/migrations/src/connection-string.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export function createConnectionString(config: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
user: string;
|
||||
db: string;
|
||||
ssl: boolean;
|
||||
}) {
|
||||
// prettier-ignore
|
||||
return `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.db}${config.ssl ? '?sslmode=require' : '?sslmode=disable'}`;
|
||||
}
|
||||
|
|
@ -4,20 +4,9 @@ import url from 'node:url';
|
|||
import { createPool } from 'slonik';
|
||||
import { SlonikMigrator } from '@slonik/migrator';
|
||||
import { migrateClickHouse } from './clickhouse';
|
||||
import { createConnectionString } from './connection-string';
|
||||
import { env } from './environment';
|
||||
|
||||
export function createConnectionString(config: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
user: string;
|
||||
db: string;
|
||||
ssl: boolean;
|
||||
}) {
|
||||
// prettier-ignore
|
||||
return `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.db}${config.ssl ? '?sslmode=require' : '?sslmode=disable'}`;
|
||||
}
|
||||
|
||||
const [, , cmd] = process.argv;
|
||||
const slonik = await createPool(createConnectionString(env.postgres));
|
||||
|
||||
|
|
|
|||
69
packages/migrations/test/drop-personal-org.test.ts
Normal file
69
packages/migrations/test/drop-personal-org.test.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import assert from 'node:assert';
|
||||
import { describe, test } from 'node:test';
|
||||
import { sql } from 'slonik';
|
||||
import { initMigrationTestingEnvironment } from './utils/testkit';
|
||||
|
||||
describe('migration: drop-personal-org', async () => {
|
||||
await test('should remove all existing personal orgs that does not have projects', async () => {
|
||||
const { db, runTo, complete, done, seed } = await initMigrationTestingEnvironment();
|
||||
|
||||
try {
|
||||
// Run migrations all the way to the point before the one we are testing
|
||||
await runTo('2023.02.21T14.32.24.supertokens-4.0.0.sql');
|
||||
|
||||
// Seed the DB with orgs
|
||||
const user = await seed.user();
|
||||
const emptyOrgs = await Promise.all([
|
||||
db.one(
|
||||
sql`INSERT INTO public.organizations (clean_id, name, user_id, type) VALUES ('personal-empty', 'personal-empty', ${user.id}, 'PERSONAL') RETURNING *;`,
|
||||
),
|
||||
db.one(
|
||||
sql`INSERT INTO public.organizations (clean_id, name, user_id, type) VALUES ('regular-empty', 'regular-empty', ${user.id}, 'REGULAR') RETURNING *;`,
|
||||
),
|
||||
]);
|
||||
const orgsWithProjects = await Promise.all([
|
||||
await db.one(
|
||||
sql`INSERT INTO public.organizations (clean_id, name, user_id, type) VALUES ('personal-project', 'personal-project', ${user.id}, 'PERSONAL') RETURNING *;`,
|
||||
),
|
||||
await db.one(
|
||||
sql`INSERT INTO public.organizations (clean_id, name, user_id, type) VALUES ('regular-project', 'regular-project', ${user.id}, 'PERSONAL') RETURNING *;`,
|
||||
),
|
||||
]);
|
||||
|
||||
// Seed with projects
|
||||
await db.one(
|
||||
sql`INSERT INTO public.projects (clean_id, name, type, org_id) VALUES ('proj-1', 'proj-1', 'SINGLE', ${orgsWithProjects[0].id}) RETURNING *;`,
|
||||
);
|
||||
await db.one(
|
||||
sql`INSERT INTO public.projects (clean_id, name, type, org_id) VALUES ('proj-2', 'proj-2', 'SINGLE', ${orgsWithProjects[1].id}) RETURNING *;`,
|
||||
);
|
||||
|
||||
// Run the additional remaining migrations
|
||||
await complete();
|
||||
|
||||
// Only this one should be deleted, the rest should still exists
|
||||
assert.equal(
|
||||
await db.maybeOne(sql`SELECT * FROM public.organizations WHERE id = ${emptyOrgs[0].id}`),
|
||||
null,
|
||||
);
|
||||
assert.notEqual(
|
||||
await db.maybeOne(sql`SELECT * FROM public.organizations WHERE id = ${emptyOrgs[1].id}`),
|
||||
null,
|
||||
);
|
||||
assert.notEqual(
|
||||
await db.maybeOne(
|
||||
sql`SELECT * FROM public.organizations WHERE id = ${orgsWithProjects[0].id}`,
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.notEqual(
|
||||
await db.maybeOne(
|
||||
sql`SELECT * FROM public.organizations WHERE id = ${orgsWithProjects[1].id}`,
|
||||
),
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await done();
|
||||
}
|
||||
});
|
||||
});
|
||||
71
packages/migrations/test/utils/testkit.ts
Normal file
71
packages/migrations/test/utils/testkit.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* eslint-disable import/first */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { config } from 'dotenv';
|
||||
import pgpFactory from 'pg-promise';
|
||||
import { createPool, sql } from 'slonik';
|
||||
import { SlonikMigrator } from '@slonik/migrator';
|
||||
import type * as DbTypes from '../../../services/storage/src/db/types';
|
||||
import { createConnectionString } from '../../src/connection-string';
|
||||
|
||||
export { DbTypes };
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
config({
|
||||
path: resolve(__dirname, '../../.env'),
|
||||
});
|
||||
|
||||
import { env } from '../../src/environment';
|
||||
|
||||
export async function initMigrationTestingEnvironment() {
|
||||
const pgp = pgpFactory();
|
||||
const db = pgp(
|
||||
createConnectionString({
|
||||
...env.postgres,
|
||||
db: 'postgres',
|
||||
}),
|
||||
);
|
||||
|
||||
const dbName = 'migration_test_' + Date.now();
|
||||
await db.query(`CREATE DATABASE ${dbName};`);
|
||||
|
||||
const slonik = await createPool(
|
||||
createConnectionString({
|
||||
...env.postgres,
|
||||
db: dbName,
|
||||
}),
|
||||
);
|
||||
|
||||
const actionsDirectory = resolve(__dirname + '/../../src/actions/');
|
||||
console.log('actionsDirectory', actionsDirectory);
|
||||
|
||||
const migrator = new SlonikMigrator({
|
||||
migrationsPath: actionsDirectory,
|
||||
slonik,
|
||||
migrationTableName: 'migration',
|
||||
logger: console,
|
||||
});
|
||||
|
||||
return {
|
||||
db: slonik,
|
||||
async runTo(name: string) {
|
||||
await migrator.up({ to: name });
|
||||
},
|
||||
seed: {
|
||||
async user() {
|
||||
return await slonik.one<DbTypes.users>(
|
||||
sql`INSERT INTO public.users (email, display_name, full_name, supertoken_user_id) VALUES ('test@mail.com', 'test1' , 'test1', '1') RETURNING *;`,
|
||||
);
|
||||
},
|
||||
},
|
||||
async complete() {
|
||||
await migrator.up();
|
||||
},
|
||||
async done(deleteDb = true) {
|
||||
deleteDb ?? (await db.query(`DROP DATABASE ${dbName};`));
|
||||
await db.$pool.end().catch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ export type {
|
|||
} from './shared/entities';
|
||||
export { minifySchema } from './shared/schema';
|
||||
export { HiveError } from './shared/errors';
|
||||
export { OrganizationType, ProjectType } from './__generated__/types';
|
||||
export { ProjectType } from './__generated__/types';
|
||||
export type { AuthProvider } from './__generated__/types';
|
||||
export { HttpClient } from './modules/shared/providers/http-client';
|
||||
export { OperationsManager } from './modules/operations/providers/operations-manager';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import zod from 'zod';
|
||||
import { OIDCIntegration, OrganizationType } from '../../../shared/entities';
|
||||
import { OIDCIntegration } from '../../../shared/entities';
|
||||
import { AuthManager } from '../../auth/providers/auth-manager';
|
||||
import { OrganizationAccessScope } from '../../auth/providers/organization-access';
|
||||
import { CryptoProvider } from '../../shared/providers/crypto';
|
||||
|
|
@ -29,17 +29,14 @@ export class OIDCIntegrationsProvider {
|
|||
return this.enabled;
|
||||
}
|
||||
|
||||
async canViewerManageIntegrationForOrganization(args: {
|
||||
organizationId: string;
|
||||
organizationType: OrganizationType;
|
||||
}) {
|
||||
if (this.isEnabled() === false || args.organizationType === OrganizationType.PERSONAL) {
|
||||
async canViewerManageIntegrationForOrganization(organizationId: string) {
|
||||
if (this.isEnabled() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authManager.ensureOrganizationAccess({
|
||||
organization: args.organizationId,
|
||||
organization: organizationId,
|
||||
scope: OrganizationAccessScope.INTEGRATIONS,
|
||||
});
|
||||
return true;
|
||||
|
|
@ -97,11 +94,8 @@ export class OIDCIntegrationsProvider {
|
|||
|
||||
const organization = await this.storage.getOrganization({ organization: args.organizationId });
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: 'Personal organizations cannot have OIDC integrations.',
|
||||
} as const;
|
||||
if (!organization) {
|
||||
throw new Error(`Failed to locate organization ${args.organizationId}`);
|
||||
}
|
||||
|
||||
const clientIdResult = OIDCIntegrationClientIdModel.safeParse(args.clientId);
|
||||
|
|
|
|||
|
|
@ -98,10 +98,9 @@ export const resolvers: OidcIntegrationsModule.Resolvers = {
|
|||
},
|
||||
Organization: {
|
||||
viewerCanManageOIDCIntegration: (organization, _, { injector }) => {
|
||||
return injector.get(OIDCIntegrationsProvider).canViewerManageIntegrationForOrganization({
|
||||
organizationId: organization.id,
|
||||
organizationType: organization.type,
|
||||
});
|
||||
return injector
|
||||
.get(OIDCIntegrationsProvider)
|
||||
.canViewerManageIntegrationForOrganization(organization.id);
|
||||
},
|
||||
oidcIntegration: async (organization, _, { injector }) => {
|
||||
if (injector.get(OIDCIntegrationsProvider).isEnabled() === false) {
|
||||
|
|
|
|||
|
|
@ -175,16 +175,10 @@ export default gql`
|
|||
organization: Organization!
|
||||
}
|
||||
|
||||
enum OrganizationType {
|
||||
PERSONAL
|
||||
REGULAR
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: ID!
|
||||
cleanId: ID!
|
||||
name: String!
|
||||
type: OrganizationType!
|
||||
owner: Member!
|
||||
me: Member!
|
||||
members: MemberConnection!
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createHash } from 'crypto';
|
||||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import { paramCase } from 'param-case';
|
||||
import { Organization, OrganizationInvitation, OrganizationType } from '../../../shared/entities';
|
||||
import { Organization, OrganizationInvitation } from '../../../shared/entities';
|
||||
import { HiveError } from '../../../shared/errors';
|
||||
import { cache, diffArrays, pushIfMissing, share, uuid } from '../../../shared/helpers';
|
||||
import { ActivityManager } from '../../activity/providers/activity-manager';
|
||||
|
|
@ -140,14 +140,13 @@ export class OrganizationManager {
|
|||
|
||||
async createOrganization(input: {
|
||||
name: string;
|
||||
type: OrganizationType;
|
||||
user: {
|
||||
id: string;
|
||||
superTokensUserId: string | null;
|
||||
oidcIntegrationId: string | null;
|
||||
};
|
||||
}): Promise<Organization> {
|
||||
const { name, type, user } = input;
|
||||
const { name, user } = input;
|
||||
this.logger.info('Creating an organization (input=%o)', input);
|
||||
|
||||
if (user.oidcIntegrationId) {
|
||||
|
|
@ -161,7 +160,6 @@ export class OrganizationManager {
|
|||
const organization = await this.storage.createOrganization({
|
||||
name,
|
||||
cleanId: paramCase(name),
|
||||
type,
|
||||
user: user.id,
|
||||
scopes: organizationAdminScopes,
|
||||
reservedNames: reservedOrganizationNames,
|
||||
|
|
@ -189,10 +187,6 @@ export class OrganizationManager {
|
|||
organization: selector.organization,
|
||||
});
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
throw new HiveError(`Cannot remove a personal organization`);
|
||||
}
|
||||
|
||||
const deletedOrganization = await this.storage.deleteOrganization({
|
||||
organization: organization.id,
|
||||
});
|
||||
|
|
@ -287,10 +281,6 @@ export class OrganizationManager {
|
|||
}),
|
||||
]);
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
throw new HiveError(`Cannot rename a personal organization`);
|
||||
}
|
||||
|
||||
let cleanId = paramCase(name);
|
||||
|
||||
if (await this.storage.getOrganizationByCleanId({ cleanId })) {
|
||||
|
|
@ -344,10 +334,6 @@ export class OrganizationManager {
|
|||
organization: input.organization,
|
||||
});
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
throw new HiveError(`Cannot invite to a personal organization`);
|
||||
}
|
||||
|
||||
const members = await this.getOrganizationMembers({ organization: input.organization });
|
||||
const existingMember = members.find(member => member.user.email === email);
|
||||
|
||||
|
|
@ -425,10 +411,6 @@ export class OrganizationManager {
|
|||
return organization;
|
||||
}
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
throw new HiveError(`Cannot join a personal organization`);
|
||||
}
|
||||
|
||||
await this.storage.addOrganizationMemberViaInvitationCode({
|
||||
code,
|
||||
user: user.id,
|
||||
|
|
@ -493,14 +475,6 @@ export class OrganizationManager {
|
|||
|
||||
const organization = await this.getOrganization(selector);
|
||||
|
||||
if (organization.type === OrganizationType.PERSONAL) {
|
||||
return {
|
||||
error: {
|
||||
message: `Personal organizations cannot be transferred`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { code } = await this.storage.createOrganizationTransferRequest({
|
||||
organization: organization.id,
|
||||
user: member.id,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { OrganizationType } from '../../shared/entities';
|
||||
import { createConnection } from '../../shared/schema';
|
||||
import { AuthManager } from '../auth/providers/auth-manager';
|
||||
import { IdTranslator } from '../shared/providers/id-translator';
|
||||
|
|
@ -80,7 +79,6 @@ export const resolvers: OrganizationModule.Resolvers = {
|
|||
const user = await injector.get(AuthManager).getCurrentUser();
|
||||
const organization = await injector.get(OrganizationManager).createOrganization({
|
||||
name: input.name,
|
||||
type: OrganizationType.REGULAR,
|
||||
user,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export interface Storage {
|
|||
getMyOrganization(_: { user: string }): Promise<Organization | null>;
|
||||
getOrganizations(_: { user: string }): Promise<readonly Organization[] | never>;
|
||||
createOrganization(
|
||||
_: Pick<Organization, 'cleanId' | 'name' | 'type'> & {
|
||||
_: Pick<Organization, 'cleanId' | 'name'> & {
|
||||
user: string;
|
||||
scopes: ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
|
||||
reservedNames: string[];
|
||||
|
|
|
|||
|
|
@ -124,11 +124,6 @@ export enum ProjectType {
|
|||
SINGLE = 'SINGLE',
|
||||
}
|
||||
|
||||
export enum OrganizationType {
|
||||
PERSONAL = 'PERSONAL',
|
||||
REGULAR = 'REGULAR',
|
||||
}
|
||||
|
||||
export interface OrganizationGetStarted {
|
||||
id: string;
|
||||
creatingProject: boolean;
|
||||
|
|
@ -143,7 +138,6 @@ export interface Organization {
|
|||
id: string;
|
||||
cleanId: string;
|
||||
name: string;
|
||||
type: OrganizationType;
|
||||
billingPlan: string;
|
||||
monthlyRateLimit: {
|
||||
retentionInDays: number;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
export type alert_channel_type = "SLACK" | "WEBHOOK";
|
||||
export type alert_type = "SCHEMA_CHANGE_NOTIFICATIONS";
|
||||
export type operation_kind = "mutation" | "query" | "subscription";
|
||||
export type organization_type = "PERSONAL" | "REGULAR";
|
||||
export type user_role = "ADMIN" | "MEMBER";
|
||||
|
||||
export interface activities {
|
||||
|
|
@ -106,7 +105,6 @@ export interface organizations {
|
|||
ownership_transfer_user_id: string | null;
|
||||
plan_name: string;
|
||||
slack_token: string | null;
|
||||
type: organization_type;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import type {
|
|||
OrganizationAccessScope,
|
||||
OrganizationBilling,
|
||||
OrganizationInvitation,
|
||||
OrganizationType,
|
||||
PersistedOperation,
|
||||
Project,
|
||||
ProjectAccessScope,
|
||||
|
|
@ -167,7 +166,6 @@ export async function createStorage(connection: string, maximumPoolSize: number)
|
|||
operations: parseInt(organization.limit_operations_monthly),
|
||||
},
|
||||
billingPlan: organization.plan_name,
|
||||
type: (organization.type === 'PERSONAL' ? 'PERSONAL' : 'REGULAR') as OrganizationType,
|
||||
getStarted: {
|
||||
id: organization.id,
|
||||
creatingProject: organization.get_started_creating_project,
|
||||
|
|
@ -488,7 +486,6 @@ export async function createStorage(connection: string, maximumPoolSize: number)
|
|||
name,
|
||||
user,
|
||||
cleanId,
|
||||
type,
|
||||
scopes,
|
||||
reservedNames,
|
||||
}: Parameters<Storage['createOrganization']>[0] & {
|
||||
|
|
@ -520,9 +517,9 @@ export async function createStorage(connection: string, maximumPoolSize: number)
|
|||
const org = await connection.one<Slonik<organizations>>(
|
||||
sql`
|
||||
INSERT INTO public.organizations
|
||||
("name", "clean_id", "type", "user_id")
|
||||
("name", "clean_id", "user_id")
|
||||
VALUES
|
||||
(${name}, ${availableCleanId}, ${type}, ${user})
|
||||
(${name}, ${availableCleanId}, ${user})
|
||||
RETURNING *
|
||||
`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ const ProjectCard_ProjectFragment = graphql(`
|
|||
fragment ProjectCard_ProjectFragment on Project {
|
||||
cleanId
|
||||
id
|
||||
type
|
||||
name
|
||||
}
|
||||
`);
|
||||
|
|
@ -83,7 +82,6 @@ const ProjectCard = (props: {
|
|||
>
|
||||
<div className="flex items-start gap-x-2">
|
||||
<div className="grow">
|
||||
<h3 className="text-xs font-medium text-[#34EAB9]">{project.type}</h3>
|
||||
<h4 className="line-clamp-2 text-lg font-bold">{project.name}</h4>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { CopyIcon, KeyIcon, MoreIcon, SettingsIcon, TrashIcon } from '@/components/v2/icon';
|
||||
import { ChangePermissionsModal, DeleteMembersModal } from '@/components/v2/modals';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { MeDocument, OrganizationFieldsFragment, OrganizationType } from '@/graphql';
|
||||
import { MeDocument, OrganizationFieldsFragment } from '@/graphql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
import { useClipboard } from '@/lib/hooks/use-clipboard';
|
||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
||||
|
|
@ -217,7 +217,6 @@ const Page_OrganizationFragment = graphql(`
|
|||
...ChangePermissionsModal_MemberFragment
|
||||
}
|
||||
cleanId
|
||||
type
|
||||
owner {
|
||||
id
|
||||
}
|
||||
|
|
@ -281,21 +280,18 @@ function Page(props: { organization: FragmentType<typeof Page_OrganizationFragme
|
|||
|
||||
const [meQuery] = useQuery({ query: MeDocument });
|
||||
const router = useRouteSelector();
|
||||
|
||||
const org = organization;
|
||||
const isPersonal = org?.type === OrganizationType.Personal;
|
||||
const members = org?.members.nodes;
|
||||
const members = organization?.members.nodes;
|
||||
|
||||
useEffect(() => {
|
||||
if (isPersonal) {
|
||||
void router.replace(`/${router.organizationId}`);
|
||||
} else if (members) {
|
||||
if (members) {
|
||||
// uncheck checkboxes when members were deleted
|
||||
setChecked(prev => prev.filter(id => members.some(node => node.id === id)));
|
||||
}
|
||||
}, [isPersonal, router, members]);
|
||||
}, [router, members]);
|
||||
|
||||
if (!org || isPersonal) return null;
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const me = meQuery.data?.me;
|
||||
const selectedMember = selectedMemberId
|
||||
|
|
@ -311,7 +307,7 @@ function Page(props: { organization: FragmentType<typeof Page_OrganizationFragme
|
|||
<ChangePermissionsModal
|
||||
isOpen={isPermissionsModalOpen}
|
||||
toggleModalOpen={togglePermissionsModalOpen}
|
||||
organization={org}
|
||||
organization={organization}
|
||||
member={selectedMember}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -321,7 +317,7 @@ function Page(props: { organization: FragmentType<typeof Page_OrganizationFragme
|
|||
memberIds={checked}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<MemberInvitationForm organizationCleanId={org.cleanId} />
|
||||
<MemberInvitationForm organizationCleanId={organization.cleanId} />
|
||||
<Button
|
||||
size="large"
|
||||
danger
|
||||
|
|
@ -336,7 +332,7 @@ function Page(props: { organization: FragmentType<typeof Page_OrganizationFragme
|
|||
{members?.map(node => {
|
||||
const IconToUse = KeyIcon;
|
||||
|
||||
const isOwner = node.id === org.owner.id;
|
||||
const isOwner = node.id === organization.owner.id;
|
||||
const isMe = node.id === me?.id;
|
||||
const isDisabled = isOwner || isMe;
|
||||
|
||||
|
|
@ -386,7 +382,7 @@ function Page(props: { organization: FragmentType<typeof Page_OrganizationFragme
|
|||
</Card>
|
||||
);
|
||||
})}
|
||||
<OrganizationInvitations organization={org} />
|
||||
<OrganizationInvitations organization={organization} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
CheckIntegrationsDocument,
|
||||
DeleteGitHubIntegrationDocument,
|
||||
DeleteSlackIntegrationDocument,
|
||||
OrganizationType,
|
||||
} from '@/graphql';
|
||||
import {
|
||||
canAccessOrganization,
|
||||
|
|
@ -144,7 +143,6 @@ const UpdateOrganizationNameMutation = graphql(`
|
|||
|
||||
const SettingsPageRenderer_OrganizationFragment = graphql(`
|
||||
fragment SettingsPageRenderer_OrganizationFragment on Organization {
|
||||
type
|
||||
name
|
||||
me {
|
||||
...CanAccessOrganization_MemberFragment
|
||||
|
|
@ -165,7 +163,6 @@ const SettingsPageRenderer = (props: {
|
|||
redirect: true,
|
||||
});
|
||||
const router = useRouteSelector();
|
||||
const isRegularOrg = organization?.type === OrganizationType.Regular;
|
||||
const [isDeleteModalOpen, toggleDeleteModalOpen] = useToggle();
|
||||
const [isTransferModalOpen, toggleTransferModalOpen] = useToggle();
|
||||
|
||||
|
|
@ -198,46 +195,44 @@ const SettingsPageRenderer = (props: {
|
|||
|
||||
return (
|
||||
<>
|
||||
{isRegularOrg && (
|
||||
<Card>
|
||||
<Heading className="mb-2">Organization Name</Heading>
|
||||
<p className="mb-3 font-light text-gray-300">
|
||||
Name of your organization visible within Hive
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="flex gap-x-2">
|
||||
<Input
|
||||
placeholder="Organization name"
|
||||
name="name"
|
||||
value={values.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.name && !!errors.name}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="large"
|
||||
disabled={isSubmitting}
|
||||
className="px-10"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
{touched.name && (errors.name || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">{errors.name || mutation.error?.message}</div>
|
||||
)}
|
||||
{mutation.data?.updateOrganizationName?.error && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data?.updateOrganizationName.error.message}
|
||||
</div>
|
||||
)}
|
||||
{mutation.error && (
|
||||
<div>{mutation.error.graphQLErrors[0]?.message ?? mutation.error.message}</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<Heading className="mb-2">Organization Name</Heading>
|
||||
<p className="mb-3 font-light text-gray-300">
|
||||
Name of your organization visible within Hive
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="flex gap-x-2">
|
||||
<Input
|
||||
placeholder="Organization name"
|
||||
name="name"
|
||||
value={values.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.name && !!errors.name}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="large"
|
||||
disabled={isSubmitting}
|
||||
className="px-10"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
{touched.name && (errors.name || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">{errors.name || mutation.error?.message}</div>
|
||||
)}
|
||||
{mutation.data?.updateOrganizationName?.error && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data?.updateOrganizationName.error.message}
|
||||
</div>
|
||||
)}
|
||||
{mutation.error && (
|
||||
<div>{mutation.error.graphQLErrors[0]?.message ?? mutation.error.message}</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{canAccessOrganization(OrganizationAccessScope.Integrations, organization.me) && (
|
||||
<Card>
|
||||
|
|
@ -249,7 +244,7 @@ const SettingsPageRenderer = (props: {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{isRegularOrg && organization.me.isOwner && (
|
||||
{organization.me.isOwner && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -276,7 +271,7 @@ const SettingsPageRenderer = (props: {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{isRegularOrg && canAccessOrganization(OrganizationAccessScope.Delete, organization.me) && (
|
||||
{canAccessOrganization(OrganizationAccessScope.Delete, organization.me) && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Title } from '@/components/common';
|
|||
import { DataWrapper } from '@/components/v2';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
import { env } from '@/env/backend';
|
||||
import { OrganizationsDocument, OrganizationType } from '@/graphql';
|
||||
import { OrganizationsDocument } from '@/graphql';
|
||||
import { writeLastVisitedOrganization } from '@/lib/cookies';
|
||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
||||
import { withSessionProtection } from '@/lib/supertokens/guard';
|
||||
|
|
@ -74,9 +74,8 @@ function Home(): ReactElement {
|
|||
useEffect(() => {
|
||||
// Just in case server-side redirect wasn't working
|
||||
if (query.data) {
|
||||
const org = query.data.organizations.nodes.find(
|
||||
node => node.type === OrganizationType.Personal,
|
||||
);
|
||||
const org = query.data.organizations.nodes[0];
|
||||
|
||||
if (org) {
|
||||
router.visitOrganization({ organizationId: org.cleanId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ const dateRangeOptions = DATE_RANGE_OPTIONS.filter(isNotAllOption);
|
|||
type FilterKey = keyof Filters;
|
||||
|
||||
const CHECKBOXES: { value: FilterKey; label: string; tooltip?: string }[] = [
|
||||
{
|
||||
value: 'only-regular',
|
||||
label: 'Only Regular',
|
||||
tooltip: 'Do not count personal organizations, created automatically for every user',
|
||||
},
|
||||
{ value: 'with-projects', label: 'With Projects' },
|
||||
{ value: 'with-targets', label: 'With Targets' },
|
||||
{ value: 'with-schema-pushes', label: 'With Schema Pushes' },
|
||||
|
|
@ -38,7 +33,6 @@ function Manage() {
|
|||
const newFilters: {
|
||||
[key in FilterKey]: boolean;
|
||||
} = {
|
||||
'only-regular': false,
|
||||
'with-collected': false,
|
||||
'with-schema-pushes': false,
|
||||
'with-persisted': false,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { Button, DataWrapper, Stat, Table, TBody, Td, Th, THead, Tr } from '@/co
|
|||
import { CHART_PRIMARY_COLOR } from '@/constants';
|
||||
import { env } from '@/env/frontend';
|
||||
import { DocumentType, FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { OrganizationType } from '@/graphql';
|
||||
import { theme } from '@/lib/charts';
|
||||
import { useChartStyles } from '@/utils';
|
||||
import { ChevronUpIcon } from '@radix-ui/react-icons';
|
||||
|
|
@ -33,7 +32,6 @@ import {
|
|||
interface Organization {
|
||||
name: ReactElement;
|
||||
members: string;
|
||||
type: OrganizationType;
|
||||
users: number;
|
||||
projects: number;
|
||||
targets: number;
|
||||
|
|
@ -58,7 +56,6 @@ function sumByKey<
|
|||
}
|
||||
|
||||
export type Filters = Partial<{
|
||||
'only-regular': boolean;
|
||||
'with-projects': boolean;
|
||||
'with-targets': boolean;
|
||||
'with-schema-pushes': boolean;
|
||||
|
|
@ -152,7 +149,6 @@ const AdminStatsQuery = graphql(`
|
|||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
owner {
|
||||
user {
|
||||
email
|
||||
|
|
@ -188,10 +184,6 @@ function filterStats(
|
|||
row: DocumentType<typeof AdminStatsQuery>['admin']['stats']['organizations'][0],
|
||||
filters: Filters,
|
||||
) {
|
||||
if (filters['only-regular'] && row.organization.type !== 'REGULAR') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters['with-projects'] && row.projects === 0) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -221,10 +213,6 @@ const columns = [
|
|||
footer: props => props.column.id,
|
||||
enableSorting: false,
|
||||
}),
|
||||
table.createDataColumn('type', {
|
||||
header: 'Type',
|
||||
footer: props => props.column.id,
|
||||
}),
|
||||
table.createDataColumn('members', {
|
||||
header: 'Members',
|
||||
footer: props => props.column.id,
|
||||
|
|
@ -412,7 +400,6 @@ export function AdminStats({
|
|||
</div>
|
||||
),
|
||||
members: (node.organization.members.nodes || []).map(v => v.user.email).join(', '),
|
||||
type: node.organization.type,
|
||||
users: node.users,
|
||||
projects: node.projects,
|
||||
targets: node.targets,
|
||||
|
|
@ -425,7 +412,7 @@ export function AdminStats({
|
|||
|
||||
const overall = useMemo(
|
||||
() => ({
|
||||
users: tableData.reduce((total, node) => (node.type === 'PERSONAL' ? total + 1 : total), 0),
|
||||
users: sumByKey(tableData, 'users'),
|
||||
organizations: tableData.length,
|
||||
projects: sumByKey(tableData, 'projects'),
|
||||
targets: sumByKey(tableData, 'targets'),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { ReactElement, ReactNode } from 'react';
|
|||
import clsx from 'clsx';
|
||||
import { Drawer } from '@/components/v2';
|
||||
import { DocumentType, graphql } from '@/gql';
|
||||
import { OrganizationType } from '@/graphql';
|
||||
import { getDocsUrl } from '@/lib/docs-url';
|
||||
import { useToggle } from '@/lib/hooks';
|
||||
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
||||
|
|
@ -20,10 +19,8 @@ const GetStartedWizard_GetStartedProgress = graphql(`
|
|||
|
||||
export function GetStartedProgress({
|
||||
tasks,
|
||||
organizationType,
|
||||
}: {
|
||||
tasks: DocumentType<typeof GetStartedWizard_GetStartedProgress>;
|
||||
organizationType: OrganizationType;
|
||||
}): ReactElement | null {
|
||||
const [isOpen, toggle] = useToggle();
|
||||
|
||||
|
|
@ -31,14 +28,7 @@ export function GetStartedProgress({
|
|||
return null;
|
||||
}
|
||||
|
||||
const processedTasks =
|
||||
organizationType === OrganizationType.Personal
|
||||
? {
|
||||
...tasks,
|
||||
invitingMembers: undefined,
|
||||
}
|
||||
: tasks;
|
||||
const values = Object.values(processedTasks).filter(v => typeof v === 'boolean');
|
||||
const values = Object.values(tasks).filter(v => typeof v === 'boolean');
|
||||
const total = values.length;
|
||||
const completed = values.filter(t => t === true).length;
|
||||
const remaining = total - completed;
|
||||
|
|
@ -66,7 +56,7 @@ export function GetStartedProgress({
|
|||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<GetStartedWizard isOpen={isOpen} onClose={toggle} tasks={processedTasks} />
|
||||
<GetStartedWizard isOpen={isOpen} onClose={toggle} tasks={tasks} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { PlusIcon } from '@/components/v2/icon';
|
|||
import { CreateProjectModal } from '@/components/v2/modals';
|
||||
import { LAST_VISITED_ORG_KEY } from '@/constants';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { Exact, OrganizationType } from '@/graphql';
|
||||
import { Exact } from '@/graphql';
|
||||
import {
|
||||
canAccessOrganization,
|
||||
OrganizationAccessScope,
|
||||
|
|
@ -26,7 +26,6 @@ enum TabValue {
|
|||
|
||||
const OrganizationLayout_OrganizationFragment = graphql(`
|
||||
fragment OrganizationLayout_OrganizationFragment on Organization {
|
||||
type
|
||||
name
|
||||
me {
|
||||
...CanAccessOrganization_MemberFragment
|
||||
|
|
@ -100,7 +99,6 @@ export function OrganizationLayout<
|
|||
}
|
||||
|
||||
const me = organization?.me;
|
||||
const isRegularOrg = !organization || organization.type === OrganizationType.Regular;
|
||||
|
||||
if (!organization || !me) {
|
||||
return null;
|
||||
|
|
@ -133,7 +131,7 @@ export function OrganizationLayout<
|
|||
<Tabs.Trigger value={TabValue.Overview} asChild>
|
||||
<NextLink href={`/${orgId}`}>Overview</NextLink>
|
||||
</Tabs.Trigger>
|
||||
{isRegularOrg && canAccessOrganization(OrganizationAccessScope.Members, me) && (
|
||||
{canAccessOrganization(OrganizationAccessScope.Members, me) && (
|
||||
<Tabs.Trigger value={TabValue.Members} asChild>
|
||||
<NextLink href={`/${orgId}/view/${TabValue.Members}`}>Members</NextLink>
|
||||
</Tabs.Trigger>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,10 @@ import {
|
|||
TrendingUpIcon,
|
||||
} from '@/components/v2/icon';
|
||||
import { env } from '@/env/frontend';
|
||||
import { MeDocument, OrganizationsDocument, OrganizationsQuery, OrganizationType } from '@/graphql';
|
||||
import { MeDocument, OrganizationsDocument } from '@/graphql';
|
||||
import { getDocsUrl } from '@/lib/docs-url';
|
||||
import { useRouteSelector } from '@/lib/hooks';
|
||||
|
||||
type DropdownOrganization = OrganizationsQuery['organizations']['nodes'];
|
||||
|
||||
export function Header(): ReactElement {
|
||||
const router = useRouteSelector();
|
||||
const [meQuery] = useQuery({ query: MeDocument });
|
||||
|
|
@ -41,25 +39,11 @@ export function Header(): ReactElement {
|
|||
const [isOpaque, setIsOpaque] = useState(false);
|
||||
|
||||
const me = meQuery.data?.me;
|
||||
const allOrgs = organizationsQuery.data?.organizations.nodes || [];
|
||||
const { personal, organizations } = allOrgs.reduce<{
|
||||
personal: DropdownOrganization;
|
||||
organizations: DropdownOrganization;
|
||||
}>(
|
||||
(acc, node) => {
|
||||
if (node.type === OrganizationType.Personal) {
|
||||
acc.personal.push(node);
|
||||
} else {
|
||||
acc.organizations.push(node);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ personal: [], organizations: [] },
|
||||
);
|
||||
const organizations = organizationsQuery.data?.organizations.nodes || [];
|
||||
|
||||
const currentOrg =
|
||||
typeof router.organizationId === 'string'
|
||||
? allOrgs.find(org => org.cleanId === router.organizationId)
|
||||
? organizations.find(org => org.cleanId === router.organizationId)
|
||||
: null;
|
||||
|
||||
// Copied from tailwindcss website
|
||||
|
|
@ -92,9 +76,7 @@ export function Header(): ReactElement {
|
|||
<div className="container flex h-[84px] items-center justify-between">
|
||||
<HiveLink />
|
||||
<div className="flex flex-row gap-8">
|
||||
{currentOrg ? (
|
||||
<GetStartedProgress organizationType={currentOrg.type} tasks={currentOrg.getStarted} />
|
||||
) : null}
|
||||
{currentOrg ? <GetStartedProgress tasks={currentOrg.getStarted} /> : null}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
|
|
@ -116,17 +98,9 @@ export function Header(): ReactElement {
|
|||
</DropdownMenuSubTrigger>
|
||||
) : null}
|
||||
<DropdownMenuSubContent sideOffset={25} className="max-w-[300px]">
|
||||
<DropdownMenuLabel className="px-2 mb-2 text-xs font-bold text-gray-500">
|
||||
PERSONAL
|
||||
</DropdownMenuLabel>
|
||||
{personal.map(org => (
|
||||
<NextLink href={`/${org.cleanId}`} key={org.cleanId}>
|
||||
<DropdownMenuItem className="truncate !block">{org.name}</DropdownMenuItem>
|
||||
</NextLink>
|
||||
))}
|
||||
{organizations.length ? (
|
||||
<DropdownMenuLabel className="px-2 mb-2 text-xs font-bold text-gray-500">
|
||||
OUTERS ORGANIZATIONS
|
||||
<DropdownMenuLabel className="px-2 mb-2 text-xs font-bold text-gray-500 truncate !block">
|
||||
ORGANIZATIONS
|
||||
</DropdownMenuLabel>
|
||||
) : null}
|
||||
{organizations.map(org => (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ const TransferOrganizationOwnership_Members = graphql(`
|
|||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
members {
|
||||
nodes {
|
||||
isOwner
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ fragment OrganizationFields on Organization {
|
|||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
plan
|
||||
me {
|
||||
...MemberFields
|
||||
|
|
@ -19,7 +18,6 @@ fragment OrganizationEssentials on Organization {
|
|||
id
|
||||
cleanId
|
||||
name
|
||||
type
|
||||
}
|
||||
|
||||
fragment ProjectFields on Project {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||
'@hive/service-common': 'packages/services/service-common/src/index.ts',
|
||||
'@graphql-hive/core': 'packages/libraries/core/src/index.ts',
|
||||
},
|
||||
exclude: [...defaultExclude, 'integration-tests'],
|
||||
exclude: [...defaultExclude, 'integration-tests', 'packages/migrations/test'],
|
||||
setupFiles: ['./serializer.ts'],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue