Add organization settings page
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
61b1a24149
commit
6862841459
@@ -31,6 +31,7 @@
|
|||||||
"react-relay": "^19.0.0",
|
"react-relay": "^19.0.0",
|
||||||
"react-router": "^7.6.0",
|
"react-router": "^7.6.0",
|
||||||
"relay-runtime": "^19.0.0",
|
"relay-runtime": "^19.0.0",
|
||||||
|
"usehooks-ts": "^3.1.1",
|
||||||
"zod": "^3.25.17"
|
"zod": "^3.25.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
BIN
apps/console2/public/github.png
Normal file
BIN
apps/console2/public/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 945 B |
BIN
apps/console2/public/policy.png
Normal file
BIN
apps/console2/public/policy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/console2/public/slack.png
Normal file
BIN
apps/console2/public/slack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
13
apps/console2/src/hooks/graph/OrganizationGraph.ts
Normal file
13
apps/console2/src/hooks/graph/OrganizationGraph.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
export const organizationViewQuery = graphql`
|
||||||
|
query OrganizationGraph_ViewQuery($organizationId: ID!) {
|
||||||
|
node(id: $organizationId) {
|
||||||
|
... on Organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
...SettingsPageFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
203
apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts
generated
Normal file
203
apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<987fdfe9c961d800699430963c5dba55>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type OrganizationGraph_ViewQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type OrganizationGraph_ViewQuery$data = {
|
||||||
|
readonly node: {
|
||||||
|
readonly id?: string;
|
||||||
|
readonly name?: string;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type OrganizationGraph_ViewQuery = {
|
||||||
|
response: OrganizationGraph_ViewQuery$data;
|
||||||
|
variables: OrganizationGraph_ViewQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "OrganizationGraph_ViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "SettingsPageFragment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "OrganizationGraph_ViewQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "logoUrl",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ConnectorConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "connectors",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ConnectorEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Connector",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "type",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "connectors(first:100)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "66677d1e24f46cd551b4201d60c5e0a2",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "OrganizationGraph_ViewQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "196e8c1fc9c2e0b3c76c8b338ed5c7f7";
|
||||||
|
|
||||||
|
export default node;
|
||||||
246
apps/console2/src/pages/organizations/SettingsPage.tsx
Normal file
246
apps/console2/src/pages/organizations/SettingsPage.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Field,
|
||||||
|
FileButton,
|
||||||
|
IconPlusLarge,
|
||||||
|
Label,
|
||||||
|
PageHeader,
|
||||||
|
Spinner,
|
||||||
|
useToast,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import type { PreloadedQuery } from "react-relay";
|
||||||
|
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
|
||||||
|
import { useFragment, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
|
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
|
||||||
|
import { useDebounceCallback } from "usehooks-ts";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import type {
|
||||||
|
SettingsPageFragment$data,
|
||||||
|
SettingsPageFragment$key,
|
||||||
|
} from "./__generated__/SettingsPageFragment.graphql";
|
||||||
|
import type { ChangeEventHandler } from "react";
|
||||||
|
import { groupBy, sprintf } from "@probo/helpers";
|
||||||
|
import type { NodeOf } from "/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryRef: PreloadedQuery<OrganizationGraph_ViewQuery>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const organizationFragment = graphql`
|
||||||
|
fragment SettingsPageFragment on Organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
connectors(first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateOrganizationMutation = graphql`
|
||||||
|
mutation SettingsPage_UpdateMutation($input: UpdateOrganizationInput!) {
|
||||||
|
updateOrganization(input: $input) {
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
logoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function SettingsPage({ queryRef }: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const organizationKey = usePreloadedQuery(
|
||||||
|
organizationViewQuery,
|
||||||
|
queryRef
|
||||||
|
).node;
|
||||||
|
const { toast } = useToast();
|
||||||
|
const organization = useFragment<SettingsPageFragment$key>(
|
||||||
|
organizationFragment,
|
||||||
|
organizationKey
|
||||||
|
);
|
||||||
|
const [updateOrganization, isUpdating] = useMutation(
|
||||||
|
updateOrganizationMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateOrganizationName = useDebounceCallback((name: string) => {
|
||||||
|
if (!name) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
updateOrganization({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const updateOrganizationLogo: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateOrganization({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
logo: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uploadables: {
|
||||||
|
"input.logo": file,
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: __("Failed to update organization logo"),
|
||||||
|
description: error.message || __("Please try again."),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<PageHeader title={__("Settings")} />
|
||||||
|
|
||||||
|
{/* Organization settings */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-base font-medium">
|
||||||
|
{__("Organization details")}
|
||||||
|
</h2>
|
||||||
|
{isUpdating && <Spinner />}
|
||||||
|
</div>
|
||||||
|
<Card padded className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>{__("Organization logo")}</Label>
|
||||||
|
<div className="flex w-max items-center gap-4">
|
||||||
|
<Avatar
|
||||||
|
src={organization.logoUrl}
|
||||||
|
name={organization.name}
|
||||||
|
size="xl"
|
||||||
|
/>
|
||||||
|
<FileButton
|
||||||
|
disabled={isUpdating}
|
||||||
|
onChange={updateOrganizationLogo}
|
||||||
|
variant="secondary"
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
{__("Change logo")}
|
||||||
|
</FileButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
readOnly={isUpdating}
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
defaultValue={organization.name}
|
||||||
|
label={__("Organization name")}
|
||||||
|
placeholder={__("Organization name")}
|
||||||
|
onChange={(e) => updateOrganizationName(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Integrations */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-base font-medium">{__("Integrations")}</h2>
|
||||||
|
<Card padded>
|
||||||
|
<Connectors
|
||||||
|
organizationId={organization.id}
|
||||||
|
connectors={organization.connectors.edges.map((edge) => edge.node)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Connectors(props: {
|
||||||
|
organizationId: string;
|
||||||
|
connectors: NodeOf<SettingsPageFragment$data["connectors"]>[];
|
||||||
|
}) {
|
||||||
|
const { __, dateTimeFormat } = useTranslate();
|
||||||
|
const fakeconnectors = [
|
||||||
|
{
|
||||||
|
id: "github",
|
||||||
|
name: "GitHub",
|
||||||
|
type: "oauth2",
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
] satisfies typeof props.connectors;
|
||||||
|
const connectors = [
|
||||||
|
{
|
||||||
|
id: "github",
|
||||||
|
name: "GitHub",
|
||||||
|
type: "oauth2",
|
||||||
|
description: __("Connect to GitHub repositories and issues"),
|
||||||
|
...fakeconnectors.find((connector) => connector.id === "github"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "slack",
|
||||||
|
name: "Slack",
|
||||||
|
type: "oauth2",
|
||||||
|
description: __("Connect to Slack workspace and channels"),
|
||||||
|
...fakeconnectors.find((connector) => connector.id === "slack"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getUrl = (connectorId: string) => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin;
|
||||||
|
const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
|
||||||
|
url.searchParams.append("organization_id", props.organizationId);
|
||||||
|
url.searchParams.append("connector_id", connectorId);
|
||||||
|
url.searchParams.append("continue", window.location.href);
|
||||||
|
return url.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{connectors.map((connector) => (
|
||||||
|
<Card key={connector.id} padded className="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<img src={`/${connector.id}.png`} alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="mr-auto">
|
||||||
|
<h3 className="text-base font-semibold">{connector.name}</h3>
|
||||||
|
<p className="text-sm text-txt-tertiary">
|
||||||
|
{connector.createdAt
|
||||||
|
? sprintf(
|
||||||
|
__("Connected on %s"),
|
||||||
|
dateTimeFormat(connector.createdAt)
|
||||||
|
)
|
||||||
|
: connector.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{connector.createdAt ? (
|
||||||
|
<div>
|
||||||
|
<Badge variant="success" size="md">
|
||||||
|
{__("Connected")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button variant="secondary" asChild>
|
||||||
|
<a href={getUrl(connector.id)}>{__("Connect")}</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts
generated
Normal file
127
apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<02bed9e8295eda48e8fa5c0ced3ca1f6>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type SettingsPageFragment$data = {
|
||||||
|
readonly connectors: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly createdAt: any;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly type: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id: string;
|
||||||
|
readonly logoUrl: string | null | undefined;
|
||||||
|
readonly name: string;
|
||||||
|
readonly " $fragmentType": "SettingsPageFragment";
|
||||||
|
};
|
||||||
|
export type SettingsPageFragment$key = {
|
||||||
|
readonly " $data"?: SettingsPageFragment$data;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ReaderFragment = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "SettingsPageFragment",
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "logoUrl",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ConnectorConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "connectors",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ConnectorEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Connector",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "type",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "connectors(first:100)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "ecff581e2f458947e8e8b830fa3d5005";
|
||||||
|
|
||||||
|
export default node;
|
||||||
123
apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts
generated
Normal file
123
apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<6dc30c35c9ce7367967d235b1a259179>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type UpdateOrganizationInput = {
|
||||||
|
logo?: any | null | undefined;
|
||||||
|
name?: string | null | undefined;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type SettingsPage_UpdateMutation$variables = {
|
||||||
|
input: UpdateOrganizationInput;
|
||||||
|
};
|
||||||
|
export type SettingsPage_UpdateMutation$data = {
|
||||||
|
readonly updateOrganization: {
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly logoUrl: string | null | undefined;
|
||||||
|
readonly name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type SettingsPage_UpdateMutation = {
|
||||||
|
response: SettingsPage_UpdateMutation$data;
|
||||||
|
variables: SettingsPage_UpdateMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "UpdateOrganizationPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updateOrganization",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Organization",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "organization",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "logoUrl",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "SettingsPage_UpdateMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "SettingsPage_UpdateMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "ac8d86332fabe20dd7ea8fef4207b75e",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "SettingsPage_UpdateMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "85f76939223253c1d102d05cd74f7538";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -168,7 +168,18 @@ function PolicyRow({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||||
<Td>{policy.title}</Td>
|
<Td>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<img
|
||||||
|
src="/policy.png"
|
||||||
|
alt=""
|
||||||
|
width={28}
|
||||||
|
height={36}
|
||||||
|
className="border-4 border-highlight rounded box-content"
|
||||||
|
/>
|
||||||
|
{policy.title}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||||
{isDraft ? __("Draft") : __("Published")}
|
{isDraft ? __("Draft") : __("Published")}
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import {
|
|||||||
import { MainLayout } from "./layouts/MainLayout";
|
import { MainLayout } from "./layouts/MainLayout";
|
||||||
import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
|
import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
|
||||||
import { lazy, Suspense, type FC, type LazyExoticComponent } from "react";
|
import { lazy, Suspense, type FC, type LazyExoticComponent } from "react";
|
||||||
import { UnAuthenticatedError } from "./providers/RelayProviders";
|
import {
|
||||||
|
relayEnvironment,
|
||||||
|
UnAuthenticatedError,
|
||||||
|
} from "./providers/RelayProviders";
|
||||||
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
|
||||||
import { type PreloadedQuery } from "react-relay";
|
import { loadQuery, type PreloadedQuery } from "react-relay";
|
||||||
import { useCleanup } from "./hooks/useDelayedEffect.ts";
|
import { useCleanup } from "./hooks/useDelayedEffect.ts";
|
||||||
import { riskRoutes } from "./routes/riskRoutes.ts";
|
import { riskRoutes } from "./routes/riskRoutes.ts";
|
||||||
import { measureRoutes } from "./routes/measureRoutes.ts";
|
import { measureRoutes } from "./routes/measureRoutes.ts";
|
||||||
import { policiesRoutes } from "./routes/policiesRoutes.ts";
|
import { policiesRoutes } from "./routes/policiesRoutes.ts";
|
||||||
|
import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts";
|
||||||
|
|
||||||
function ErrorBoundary() {
|
function ErrorBoundary() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
@@ -64,6 +68,15 @@ const routes = [
|
|||||||
Component: MainLayout,
|
Component: MainLayout,
|
||||||
ErrorBoundary: ErrorBoundary,
|
ErrorBoundary: ErrorBoundary,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "settings",
|
||||||
|
fallback: PageSkeleton,
|
||||||
|
queryLoader: ({ organizationId }) =>
|
||||||
|
loadQuery(relayEnvironment, organizationViewQuery, {
|
||||||
|
organizationId,
|
||||||
|
}),
|
||||||
|
Component: lazy(() => import("./pages/organizations/SettingsPage")),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "vendors",
|
path: "vendors",
|
||||||
fallback: PageSkeleton,
|
fallback: PageSkeleton,
|
||||||
|
|||||||
2
apps/console2/src/vite-env.d.ts
vendored
2
apps/console2/src/vite-env.d.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly API_URL: string;
|
readonly VITE_API_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
probod:
|
probod:
|
||||||
api:
|
api:
|
||||||
cors:
|
cors:
|
||||||
allowed-origins: ["http://localhost:3000"]
|
allowed-origins: ["http://localhost:5173"]
|
||||||
aws:
|
aws:
|
||||||
access-key-id: "probod"
|
access-key-id: "probod"
|
||||||
secret-access-key: "thisisnotasecret"
|
secret-access-key: "thisisnotasecret"
|
||||||
|
|||||||
@@ -6,20 +6,21 @@ import type {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { tv, type VariantProps } from "tailwind-variants";
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
import { Slot } from "../Slot";
|
||||||
|
|
||||||
export const button = tv({
|
export const button = tv({
|
||||||
base: "flex items-center justify-center gap-[6px] px-3 py-2 rounded-full cursor-pointer text-sm font-medium h-8 focus:outline-none",
|
base: "flex items-center justify-center gap-[6px] px-3 py-2 rounded-full cursor-pointer text-sm font-medium h-8 focus:outline-none",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
primary:
|
||||||
"bg-primary text-invert hover:bg-primary-hover shadow-base hover:shadow-hover active:bg-primary-pressed focus:shadow-focus",
|
"bg-primary text-invert hover:bg-primary-hover shadow-base hover:shadow-hover active:bg-primary-pressed",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-txt-primary hover:bg-secondary-hover shadow-base hover:shadow-hover active:bg-secondary-pressed focus:shadow-focus border border-border-low",
|
"bg-secondary text-txt-primary hover:bg-secondary-hover shadow-base hover:shadow-hover active:bg-secondary-pressed border border-border-low",
|
||||||
tertiary:
|
tertiary:
|
||||||
"bg-tertiary text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed focus:shadow-focus",
|
"bg-tertiary text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed",
|
||||||
quaternary:
|
quaternary:
|
||||||
"bg-highlight text-txt-primary hover:bg-highlight-hover active:bg-highlight-pressed",
|
"bg-highlight text-txt-primary hover:bg-highlight-hover active:bg-highlight-pressed",
|
||||||
danger: "bg-danger-plain text-txt-invert hover:bg-danger-hover shadow-base hover:shadow-hover active:bg-danger-pressed focus:shadow-focus border border-border-danger",
|
danger: "bg-danger-plain text-txt-invert hover:bg-danger-hover shadow-base hover:shadow-hover active:bg-danger-pressed border border-border-danger",
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
true: "opacity-60 cursor-default",
|
true: "opacity-60 cursor-default",
|
||||||
@@ -48,6 +49,7 @@ type Props = PropsWithChildren<
|
|||||||
| "quaternary"
|
| "quaternary"
|
||||||
| "danger";
|
| "danger";
|
||||||
to?: string;
|
to?: string;
|
||||||
|
asChild?: boolean;
|
||||||
} & VariantProps<typeof button>
|
} & VariantProps<typeof button>
|
||||||
> &
|
> &
|
||||||
(
|
(
|
||||||
@@ -56,15 +58,27 @@ type Props = PropsWithChildren<
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Button = (props: Props) => {
|
export const Button = (props: Props) => {
|
||||||
const Component = props.to ? Link : "button";
|
|
||||||
const {
|
const {
|
||||||
icon: IconComponent,
|
icon: IconComponent,
|
||||||
iconAfter: IconAfterComponent,
|
iconAfter: IconAfterComponent,
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
variant,
|
variant,
|
||||||
|
asChild,
|
||||||
...componentProps
|
...componentProps
|
||||||
} = props;
|
} = props;
|
||||||
|
if (asChild) {
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
{...componentProps}
|
||||||
|
className={button({ ...props, variant, empty: !children })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Slot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = props.to ? Link : "button";
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error Component is too dynamic
|
// @ts-expect-error Component is too dynamic
|
||||||
<Component
|
<Component
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const card = tv({
|
|||||||
base: "border border-border-low bg-level-1 rounded-2xl",
|
base: "border border-border-low bg-level-1 rounded-2xl",
|
||||||
variants: {
|
variants: {
|
||||||
padded: {
|
padded: {
|
||||||
true: "p-6",
|
true: "p-4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -241,9 +241,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
a:focus-visible,
|
||||||
box-shadow: var(--shadow-focus);
|
button:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
&:not(:hover) {
|
||||||
|
box-shadow: var(--shadow-focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user