From 6862841459972d48b21779394f27354d8542b597 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 27 May 2025 14:14:20 +0200 Subject: [PATCH] Add organization settings page Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- apps/console2/package.json | 1 + apps/console2/public/github.png | Bin 0 -> 945 bytes apps/console2/public/policy.png | Bin 0 -> 1300 bytes apps/console2/public/slack.png | Bin 0 -> 1521 bytes .../src/hooks/graph/OrganizationGraph.ts | 13 + .../OrganizationGraph_ViewQuery.graphql.ts | 203 +++++++++++++++ .../src/pages/organizations/SettingsPage.tsx | 246 ++++++++++++++++++ .../SettingsPageFragment.graphql.ts | 127 +++++++++ .../SettingsPage_UpdateMutation.graphql.ts | 123 +++++++++ .../organizations/policies/PoliciesPage.tsx | 13 +- apps/console2/src/routes.tsx | 17 +- apps/console2/src/vite-env.d.ts | 2 +- cfg/dev.yaml | 2 +- packages/ui/src/Atoms/Button/Button.tsx | 24 +- packages/ui/src/Atoms/Card/Card.tsx | 2 +- packages/ui/src/theme.css | 7 +- 16 files changed, 767 insertions(+), 13 deletions(-) create mode 100644 apps/console2/public/github.png create mode 100644 apps/console2/public/policy.png create mode 100644 apps/console2/public/slack.png create mode 100644 apps/console2/src/hooks/graph/OrganizationGraph.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/SettingsPage.tsx create mode 100644 apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts create mode 100644 apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts diff --git a/apps/console2/package.json b/apps/console2/package.json index 1476a64db..c163e176f 100644 --- a/apps/console2/package.json +++ b/apps/console2/package.json @@ -31,6 +31,7 @@ "react-relay": "^19.0.0", "react-router": "^7.6.0", "relay-runtime": "^19.0.0", + "usehooks-ts": "^3.1.1", "zod": "^3.25.17" }, "devDependencies": { diff --git a/apps/console2/public/github.png b/apps/console2/public/github.png new file mode 100644 index 0000000000000000000000000000000000000000..b1532569d7a2f8b29958c5450fa3560b91d60767 GIT binary patch literal 945 zcmV;i15W&jP)A=j~?~G4RigUJ*txpd7Wj14s zq}{i>l2#}p6;k&1hvw{QMkQqw(THAwlCDhmjXizR7yUNtlrX~+Dkx^%sRt42|x(^?Ie_@kr0H ziGZuJF1S0zVGaNxHZiI#r7ECSBHuZHW1tGGhM`cv5mW`UW2Ytp*wR%nu2&4a-xg(U zIpMn#Bg8%=(6_4`pG+qH1R!-K@O<54c&^H78-ZiG4o60NjVXb88lf^(r4|(8Ot4z5 zTE{`{k2WSh1Q2)b*NJGDFh((2@PeoRcRNr8&?Pd^3sP1iD)2|ObqBU-g-&)`DwB3 zoa{otDs4g$fM8ja*K!*I=&RkVv;<=R+Zhn4ex%a1MH{4?%jVY*aeH||1U~o&8kmS{ zrVmu3NA_WO5H^3}U&=4ch7<*nKKVa3;&y&G#wT1B)IiKGkNbXCTp1tJhLpe;$#=v4 z8o#4Xh+F%dMiGCrue-ZDSzKJyyU*v7&CN|YI5?2q-Ca36Jk)o4dwcJN6PKy}i@E zZoawPtuLz8sycD;Y=dC+dcEy`XCj--KDS=2=AiWX`B^HJiVO`6$<@`BjE|3NGJ?Im zy-BTBlRzLKudlD-_xq(>F3Z5cfPUwm%jF~-4ojg>P@3oF=Gx-7A!HB%NQhPh@p`=S z@bI8X37E}hH7MV>C;-2akr7Q03>psN*u3jGUaDNFtHYI6O1{^z_t`&_U_T%ZqqC9!*BIGcz-a0>p!ZgW__V zrf7U07Yc>6z(^#b#gLdg{;;sJveF+R+TbufJ*~m5UQkT%Qn4g{pI;k_ap*ics?$q` zhlgcsY)pN)^R1gXG{kXYVnQn~7K<`DIVnp^OPVyDNh@lQtgf!g<>jSn&iC8fTh-1o zK~up(r?t&PI`r)9Oq&5(W@l%$Q$xX!VGMUsn-O5~rLeTLYb||Ksj%^8Pf94#G5eNoUQ#&7RWjMFaIg&s-!J(5m$GC7PtjwTi@*&xrGCdRO3&THw2i)J+MOqx> zAh3xkq;=_HuA(u;@}7bs8xfVEa|dZl$Z~}6XagN~hd?jhV|E9i7~VJP4J`_U6l)iA ze64>^gc+kxtzI*ZUeyC7-HQDD{9IzOn074`Qv|Ec_4T#v?Ci9KX-(2eY1>LuDiu_hr*Gd3(uV2E zAxWpQGGfHHn=(@c4WSFfB(}>Py9oMWj6*|~y=Jn`l_sAr%FWG<27sEcU(Xccwbqgn_Hf?eogg^jtJbv^3;EIr+< z^#tA-!*SGa`5+%P>)R`aTRz!H_^*7X8Lmgo^-tsXTH0KHk?%h-^-X>5FJBA*0000< KMNUMnLSTaD?q(1G literal 0 HcmV?d00001 diff --git a/apps/console2/public/slack.png b/apps/console2/public/slack.png new file mode 100644 index 0000000000000000000000000000000000000000..76e0345a508f37e6cdfd96a97aa9705eae5f3870 GIT binary patch literal 1521 zcmVe^99I9@|8lJd^grWG#>2<1w)Zi#d@)*ITgv_ zWI}>eFVg5Min3swCn&M7E2Khg5=B95^e7%IG$=!g5F2F{RuNf};fB7fhND+T@pCeV z8lQ-*ixuo!<}=x}TsepK-U~?OGFW#{1V`6AhuX3boIqx1Y zY0eWjN8>ni;}^KaUFBaKMQ9vjeKBJ-~FOG~89@L;$gMiO0C6l2UP3u_b^OMz(eRtCllB;$EuXfc+0_1`pe((Zbn=$9UKX z7SG5Shr6kc7upNZtl2rG(rMK8M&?hCqAWQwaW4k_>?$|eSDAfT)8>uG3d zbB6NYz6uCBrC)oqZc9Myolh`w>O8dMoj>e*tO3=Zy=8nKyL1f$`#xX-2FD%U{v^Uj zUYQW6t0@d$d>do8&cQCe`(K2A^_yHfu7JexFOd4^Ys-Eb^?!H1XA(Dj>O13p@rQ31 z_alECE6RBybG03!5=7a`{j=I}7cq_eto@&xXQn{<+nV=ZXu zn_PQ|*?X65vnASA8YK}Gd!L3I#Ijm$MsdQN{YPd6EyUE12Tg6>x^2b?&jtUB0Clk2 z)JFH2o;EDn*a5ec?2T@2=rR-04!4x_m2{qoaEogJ3QMV$4`&BEL8-9JCX{X5;#xpp z$EIof1QFc(lyR6N0lwvp)A}jTdzNm2TSQio>}6#eIE9>~eWI{q(|zdR^9Ub)dBUcA z^&umL7ItLXVS^i5jnwKCs$jC$>lcMsB6hmGth|W|1`CPoSQf+S;Z9N0wJy3@Xi)7K z5;{dzDC=ZJvqUcE?Da`0$ZL6I)ogE7uY35N4$zRLgiOUlHj(A z=@d%uRMe&JM XE1^!GXw@$R00000NkvXXu0mjfYctcK literal 0 HcmV?d00001 diff --git a/apps/console2/src/hooks/graph/OrganizationGraph.ts b/apps/console2/src/hooks/graph/OrganizationGraph.ts new file mode 100644 index 000000000..6f924e88b --- /dev/null +++ b/apps/console2/src/hooks/graph/OrganizationGraph.ts @@ -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 + } + } + } +`; diff --git a/apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts new file mode 100644 index 000000000..681041f79 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -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; diff --git a/apps/console2/src/pages/organizations/SettingsPage.tsx b/apps/console2/src/pages/organizations/SettingsPage.tsx new file mode 100644 index 000000000..8706ff8e0 --- /dev/null +++ b/apps/console2/src/pages/organizations/SettingsPage.tsx @@ -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; +}; + +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( + 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 = (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 ( +
+ + + {/* Organization settings */} +
+
+

+ {__("Organization details")} +

+ {isUpdating && } +
+ +
+ +
+ + + {__("Change logo")} + +
+
+ updateOrganizationName(e.currentTarget.value)} + /> +
+
+ + {/* Integrations */} +
+

{__("Integrations")}

+ + edge.node)} + /> + +
+
+ ); +} + +function Connectors(props: { + organizationId: string; + connectors: NodeOf[]; +}) { + 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 ( +
+ {connectors.map((connector) => ( + +
+ +
+
+

{connector.name}

+

+ {connector.createdAt + ? sprintf( + __("Connected on %s"), + dateTimeFormat(connector.createdAt) + ) + : connector.description} +

+
+ {connector.createdAt ? ( +
+ + {__("Connected")} + +
+ ) : ( + + )} +
+ ))} +
+ ); +} diff --git a/apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts b/apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts new file mode 100644 index 000000000..ae6dabe92 --- /dev/null +++ b/apps/console2/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts @@ -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; diff --git a/apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts b/apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts new file mode 100644 index 000000000..5eeb00c44 --- /dev/null +++ b/apps/console2/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts @@ -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; diff --git a/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx b/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx index 3e4e29b69..be2adb955 100644 --- a/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx +++ b/apps/console2/src/pages/organizations/policies/PoliciesPage.tsx @@ -168,7 +168,18 @@ function PolicyRow({ return ( <> - {policy.title} + +
+ + {policy.title} +
+ {isDraft ? __("Draft") : __("Published")} diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index 2471321b9..5d8b2f450 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -7,13 +7,17 @@ import { import { MainLayout } from "./layouts/MainLayout"; import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui"; 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 { type PreloadedQuery } from "react-relay"; +import { loadQuery, type PreloadedQuery } from "react-relay"; import { useCleanup } from "./hooks/useDelayedEffect.ts"; import { riskRoutes } from "./routes/riskRoutes.ts"; import { measureRoutes } from "./routes/measureRoutes.ts"; import { policiesRoutes } from "./routes/policiesRoutes.ts"; +import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts"; function ErrorBoundary() { const error = useRouteError(); @@ -64,6 +68,15 @@ const routes = [ Component: MainLayout, ErrorBoundary: ErrorBoundary, children: [ + { + path: "settings", + fallback: PageSkeleton, + queryLoader: ({ organizationId }) => + loadQuery(relayEnvironment, organizationViewQuery, { + organizationId, + }), + Component: lazy(() => import("./pages/organizations/SettingsPage")), + }, { path: "vendors", fallback: PageSkeleton, diff --git a/apps/console2/src/vite-env.d.ts b/apps/console2/src/vite-env.d.ts index 5697923dd..29c29a188 100644 --- a/apps/console2/src/vite-env.d.ts +++ b/apps/console2/src/vite-env.d.ts @@ -1,7 +1,7 @@ /// interface ImportMetaEnv { - readonly API_URL: string; + readonly VITE_API_URL: string; } interface ImportMeta { diff --git a/cfg/dev.yaml b/cfg/dev.yaml index 6cb96885e..2cd7a9cf1 100644 --- a/cfg/dev.yaml +++ b/cfg/dev.yaml @@ -1,7 +1,7 @@ probod: api: cors: - allowed-origins: ["http://localhost:3000"] + allowed-origins: ["http://localhost:5173"] aws: access-key-id: "probod" secret-access-key: "thisisnotasecret" diff --git a/packages/ui/src/Atoms/Button/Button.tsx b/packages/ui/src/Atoms/Button/Button.tsx index 4d2ad8d27..29b59bd72 100644 --- a/packages/ui/src/Atoms/Button/Button.tsx +++ b/packages/ui/src/Atoms/Button/Button.tsx @@ -6,20 +6,21 @@ import type { } from "react"; import { Link } from "react-router"; import { tv, type VariantProps } from "tailwind-variants"; +import { Slot } from "../Slot"; 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", variants: { variant: { 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: - "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: - "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: "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: { true: "opacity-60 cursor-default", @@ -48,6 +49,7 @@ type Props = PropsWithChildren< | "quaternary" | "danger"; to?: string; + asChild?: boolean; } & VariantProps > & ( @@ -56,15 +58,27 @@ type Props = PropsWithChildren< ); export const Button = (props: Props) => { - const Component = props.to ? Link : "button"; const { icon: IconComponent, iconAfter: IconAfterComponent, children, onClick, variant, + asChild, ...componentProps } = props; + if (asChild) { + return ( + + {children} + + ); + } + + const Component = props.to ? Link : "button"; return ( // @ts-expect-error Component is too dynamic