diff --git a/web/sdk/admin/utils/helper.ts b/web/sdk/admin/utils/helper.ts
index c8b97df75..90bd9c197 100644
--- a/web/sdk/admin/utils/helper.ts
+++ b/web/sdk/admin/utils/helper.ts
@@ -41,6 +41,13 @@ export function randomSuffix(length = 4): string {
return out;
}
+// Role titles come prefixed with the scope (e.g. "Organization Owner").
+// For display we only want the bare title: "Owner", "Member", "Admin".
+export function formatRoleTitle(title?: string): string {
+ if (!title) return '';
+ return title.replace(/^organization\s+/i, '');
+}
+
export function convertBillingAddressToString(address?: {
line1?: string;
line2?: string;
diff --git a/web/sdk/admin/views/organizations/details/apis/apis.module.css b/web/sdk/admin/views/organizations/details/apis/apis.module.css
index ecc31ad20..54401a3e1 100644
--- a/web/sdk/admin/views/organizations/details/apis/apis.module.css
+++ b/web/sdk/admin/views/organizations/details/apis/apis.module.css
@@ -7,6 +7,14 @@
text-wrap: auto;
}
+.zero-state-container {
+ padding: var(--rs-space-10);
+}
+
+.zero-state {
+ padding-top: var(--rs-space-17);
+}
+
.table {
width: 100%;
table-layout: fixed;
diff --git a/web/sdk/admin/views/organizations/details/apis/index.tsx b/web/sdk/admin/views/organizations/details/apis/index.tsx
index 0d2b00738..bffbf467a 100644
--- a/web/sdk/admin/views/organizations/details/apis/index.tsx
+++ b/web/sdk/admin/views/organizations/details/apis/index.tsx
@@ -36,6 +36,20 @@ const NoCredentials = () => {
);
};
+const ZeroState = () => {
+ return (
+
+ }
+ heading="API Credentials"
+ subHeading="Service accounts provide programmatic API access on behalf of this organization."
+ />
+
+ );
+};
+
const ErrorState = () => {
return (
: }
+ zeroState={}
classNames={{
root: styles["table-wrapper"],
table: styles["table"],
diff --git a/web/sdk/admin/views/organizations/details/invoices/columns.tsx b/web/sdk/admin/views/organizations/details/invoices/columns.tsx
index 47ec8f15a..9a47eb7b6 100644
--- a/web/sdk/admin/views/organizations/details/invoices/columns.tsx
+++ b/web/sdk/admin/views/organizations/details/invoices/columns.tsx
@@ -75,7 +75,12 @@ export const getColumns = ({
cell: ({ getValue }) => {
const value = getValue() as string;
return (
-
+
View Invoice
);
diff --git a/web/sdk/admin/views/organizations/details/invoices/index.tsx b/web/sdk/admin/views/organizations/details/invoices/index.tsx
index 76eff1d09..24cc11bf2 100644
--- a/web/sdk/admin/views/organizations/details/invoices/index.tsx
+++ b/web/sdk/admin/views/organizations/details/invoices/index.tsx
@@ -70,6 +70,20 @@ const NoInvoices = () => {
);
};
+const ZeroState = () => {
+ return (
+
+ }
+ heading="Invoices"
+ subHeading="Invoices generated for this organization's billing activity will appear here."
+ />
+
+ );
+};
+
const ErrorState = () => {
const t = useTerminology();
return (
@@ -180,6 +194,7 @@ export function OrganizationInvoicesView() {
: }
+ zeroState={}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
diff --git a/web/sdk/admin/views/organizations/details/invoices/invoices.module.css b/web/sdk/admin/views/organizations/details/invoices/invoices.module.css
index 3fc52a098..1ef5c9ce0 100644
--- a/web/sdk/admin/views/organizations/details/invoices/invoices.module.css
+++ b/web/sdk/admin/views/organizations/details/invoices/invoices.module.css
@@ -7,6 +7,14 @@
text-wrap: auto;
}
+.zero-state-container {
+ padding: var(--rs-space-10);
+}
+
+.zero-state {
+ padding-top: var(--rs-space-17);
+}
+
.table {
width: 100%;
table-layout: fixed;
@@ -26,3 +34,7 @@
.first-column {
padding-left: var(--rs-space-7);
}
+
+.invoice-link {
+ text-decoration: none;
+}
diff --git a/web/sdk/admin/views/organizations/details/members/columns.tsx b/web/sdk/admin/views/organizations/details/members/columns.tsx
index 50a2ce1ec..02ad05152 100644
--- a/web/sdk/admin/views/organizations/details/members/columns.tsx
+++ b/web/sdk/admin/views/organizations/details/members/columns.tsx
@@ -17,6 +17,7 @@ import styles from "./members.module.css";
import { DotsHorizontalIcon, UpdateIcon } from "@radix-ui/react-icons";
import { DeleteIcon } from "~/admin/assets/icons/DeleteIcon";
import { formatTimestamp, TimeStamp } from "~/admin/utils/connect-timestamp";
+import { formatRoleTitle } from "../../../../utils/helper";
const MemberStates = {
enabled: "Active",
@@ -46,7 +47,7 @@ export const getColumns = ({
const roleMap = roles.reduce(
(acc, role) => {
const id = role?.id ?? "";
- acc[id] = role.title || "";
+ acc[id] = formatRoleTitle(role.title);
return acc;
},
{} as Record,
diff --git a/web/sdk/admin/views/organizations/details/members/index.tsx b/web/sdk/admin/views/organizations/details/members/index.tsx
index a706ebb3c..7f80fdadd 100644
--- a/web/sdk/admin/views/organizations/details/members/index.tsx
+++ b/web/sdk/admin/views/organizations/details/members/index.tsx
@@ -54,6 +54,20 @@ const NoMembers = () => {
);
};
+const ZeroState = () => {
+ return (
+
+ }
+ heading="Members"
+ subHeading="Members are users who belong to this organization and can access its resources."
+ />
+
+ );
+};
+
const ErrorState = () => {
return (
: }
+ zeroState={}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
diff --git a/web/sdk/admin/views/organizations/details/members/members.module.css b/web/sdk/admin/views/organizations/details/members/members.module.css
index b74e1e389..b68df4bfe 100644
--- a/web/sdk/admin/views/organizations/details/members/members.module.css
+++ b/web/sdk/admin/views/organizations/details/members/members.module.css
@@ -15,6 +15,14 @@
text-wrap: auto;
}
+.zero-state-container {
+ padding: var(--rs-space-10);
+}
+
+.zero-state {
+ padding-top: var(--rs-space-17);
+}
+
.table {
width: 100%;
table-layout: fixed;
@@ -34,3 +42,14 @@
.table-action-column {
width: var(--rs-space-12);
}
+
+.table-action-column > * {
+ opacity: 0;
+ transition: opacity 120ms ease-in-out;
+}
+
+.table-wrapper tr:hover .table-action-column > *,
+.table-action-column:focus-within > *,
+.table-action-column:has([data-popup-open]) > * {
+ opacity: 1;
+}
diff --git a/web/sdk/admin/views/organizations/details/pat/components/pat-details-dialog.tsx b/web/sdk/admin/views/organizations/details/pat/components/pat-details-dialog.tsx
index c4d66f0bf..8739d5447 100644
--- a/web/sdk/admin/views/organizations/details/pat/components/pat-details-dialog.tsx
+++ b/web/sdk/admin/views/organizations/details/pat/components/pat-details-dialog.tsx
@@ -99,7 +99,6 @@ export function PatDetailsDialog({
{pat?.title || ""}
-
diff --git a/web/sdk/admin/views/organizations/details/projects/columns.tsx b/web/sdk/admin/views/organizations/details/projects/columns.tsx
index 3a14a59d2..27d24895c 100644
--- a/web/sdk/admin/views/organizations/details/projects/columns.tsx
+++ b/web/sdk/admin/views/organizations/details/projects/columns.tsx
@@ -5,6 +5,7 @@ import {
Flex,
Text,
Menu,
+ IconButton,
} from "@raystack/apsara";
import type { DataTableColumnDef } from "@raystack/apsara";
import type {
@@ -45,6 +46,7 @@ interface AddMemberDropdownProps {
eligibleMembers: User[];
isLoading: boolean;
setSearchQuery: (query: string) => void;
+ disabled?: boolean;
}
function AddMemberDropdown({
@@ -52,6 +54,7 @@ function AddMemberDropdown({
eligibleMembers,
isLoading,
setSearchQuery,
+ disabled,
}: AddMemberDropdownProps) {
const t = useTerminology();
return (
@@ -60,7 +63,7 @@ function AddMemberDropdown({
autocompleteMode="manual"
onInputValueChange={setSearchQuery}
>
-
+
Add {t.member({ case: "lower" })}
@@ -96,12 +99,14 @@ function ProjectActionsContent({
project,
handleProjectUpdate,
handleRenameOptionOpen,
+ canAddMember,
}: {
project: SearchOrganizationProjectsResponse_OrganizationProject;
handleProjectUpdate: (
project: SearchOrganizationProjectsResponse_OrganizationProject,
) => void;
handleRenameOptionOpen: () => void;
+ canAddMember: boolean;
}) {
const t = useTerminology();
const handleRenameOptionClick = (e: React.MouseEvent) => {
@@ -130,6 +135,7 @@ function ProjectActionsContent({
eligibleMembers={eligibleMembers}
isLoading={isLoading}
setSearchQuery={setSearchQuery}
+ disabled={!canAddMember}
/>
void;
+ canAddMember: boolean;
}) {
const [open, setOpen] = useState(false);
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
- const preventClickBubbling = (e: React.MouseEvent) => {
+ const preventClickBubbling = (e: React.MouseEvent) => {
e.stopPropagation();
};
@@ -182,10 +190,13 @@ function ProjectActions({
@@ -216,6 +228,10 @@ export const getColumns = ({
SearchOrganizationProjectsResponse_OrganizationProject,
unknown
>[] => {
+ // Adding a project member requires searching org members. With a single org
+ // member there is no one to add, so the action is disabled until more members
+ // are invited.
+ const canAddMember = Object.keys(orgMembersMap).length > 1;
return [
{
accessorKey: "title",
@@ -289,6 +305,7 @@ export const getColumns = ({
);
},
diff --git a/web/sdk/admin/views/organizations/details/projects/index.tsx b/web/sdk/admin/views/organizations/details/projects/index.tsx
index 9cd2a9e86..a8847c3bb 100644
--- a/web/sdk/admin/views/organizations/details/projects/index.tsx
+++ b/web/sdk/admin/views/organizations/details/projects/index.tsx
@@ -51,6 +51,21 @@ const NoProjects = () => {
);
};
+const ZeroState = () => {
+ const t = useTerminology();
+ return (
+
+ }
+ heading={t.project({ plural: true, case: "capital" })}
+ subHeading={`${t.project({ plural: true, case: "capital" })} group resources and members within this ${t.organization({ case: "lower" })}.`}
+ />
+
+ );
+};
+
const ErrorState = () => {
const t = useTerminology();
return (
@@ -168,12 +183,15 @@ export function OrganizationProjectsView() {
const columns = getColumns({ orgMembersMap, handleProjectUpdate, t });
+ const canAddMember = Object.keys(orgMembersMap).length > 1;
+
return (
<>
{memberDialogConfig.open && memberDialogConfig.projectId ? (
) : null}
@@ -192,6 +210,7 @@ export function OrganizationProjectsView() {
: }
+ zeroState={}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
diff --git a/web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx b/web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx
index 457dd690c..184d5c3c3 100644
--- a/web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx
+++ b/web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx
@@ -2,7 +2,6 @@ import {
Avatar,
Button,
Menu,
- Flex,
getAvatarColor,
Text,
} from "@raystack/apsara";
@@ -14,6 +13,7 @@ import { useAddProjectMembers } from "../use-add-project-members";
interface AddMembersDropdownProps {
projectId: string;
refetchMembers: () => Promise;
+ disabled?: boolean;
}
function Loader() {
@@ -31,6 +31,7 @@ function Loader() {
export function AddMembersDropdown({
projectId,
refetchMembers,
+ disabled,
}: AddMembersDropdownProps) {
const { eligibleMembers, isLoading, addMember, setSearchQuery } =
useAddProjectMembers({
@@ -52,7 +53,11 @@ export function AddMembersDropdown({
autocompleteMode="manual"
>
Add member}
+ render={
+
+ }
/>
- } />
+
+
+
+ }
+ />
();
export const ProjectMembersDialog = ({
projectId,
onClose,
+ canAddMember,
}: {
projectId: string;
onClose: () => void;
+ canAddMember: boolean;
}) => {
+ const initialFocusRef = useRef(null);
+
const [tableQuery, setTableQuery] = useDebouncedState<{
query: DataTableQuery;
rqlRequest: RQLRequest;
@@ -199,14 +203,16 @@ export const ProjectMembersDialog = ({
/>
) : null}
* {
+ opacity: 0;
+ transition: opacity 120ms ease-in-out;
+}
+
+.table-wrapper tr:hover .table-action-column>*,
+.table-action-column:focus-within>*,
+.table-action-column:has([data-popup-open])>* {
+ opacity: 1;
+}
+
+.table-wrapper tr:has([data-test-id="admin-project-member-action-menu"][data-popup-open]) {
+ background: var(--rs-color-background-base-primary-hover);
}
.table-action-dropdown {
diff --git a/web/sdk/admin/views/organizations/details/projects/projects.module.css b/web/sdk/admin/views/organizations/details/projects/projects.module.css
index 98114a4f8..c9415bbf1 100644
--- a/web/sdk/admin/views/organizations/details/projects/projects.module.css
+++ b/web/sdk/admin/views/organizations/details/projects/projects.module.css
@@ -15,6 +15,14 @@
text-wrap: auto;
}
+.zero-state-container {
+ padding: var(--rs-space-10);
+}
+
+.zero-state {
+ padding-top: var(--rs-space-17);
+}
+
.table {
width: 100%;
table-layout: fixed;
@@ -35,6 +43,22 @@
width: var(--rs-space-12);
}
+.table-action-column > * {
+ opacity: 0;
+ transition: opacity 120ms ease-in-out;
+}
+
+.table-wrapper tr:hover .table-action-column > *,
+.table-action-column:focus-within > *,
+.table-action-column:has([data-popup-open]) > * {
+ opacity: 1;
+}
+
+/* Keep the row highlighted while its action menu is open. */
+.table-wrapper tr:has([data-test-id="admin-project-actions"][data-popup-open]) {
+ background: var(--rs-color-background-base-primary-hover);
+}
+
.flex1 {
flex: 1;
}
diff --git a/web/sdk/admin/views/organizations/details/tokens/index.tsx b/web/sdk/admin/views/organizations/details/tokens/index.tsx
index 31f953bcd..a7ca7c424 100644
--- a/web/sdk/admin/views/organizations/details/tokens/index.tsx
+++ b/web/sdk/admin/views/organizations/details/tokens/index.tsx
@@ -45,6 +45,20 @@ const NoTokens = () => {
);
};
+const ZeroState = () => {
+ return (
+
+ }
+ heading="Tokens"
+ subHeading="Token transactions for this organization's consumable balance will appear here."
+ />
+
+ );
+};
+
const ErrorState = () => {
const t = useTerminology();
return (
@@ -150,6 +164,7 @@ export function OrganizationTokensView() {
: }
+ zeroState={}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
diff --git a/web/sdk/admin/views/organizations/details/tokens/tokens.module.css b/web/sdk/admin/views/organizations/details/tokens/tokens.module.css
index 11cd46935..0bfa4a70e 100644
--- a/web/sdk/admin/views/organizations/details/tokens/tokens.module.css
+++ b/web/sdk/admin/views/organizations/details/tokens/tokens.module.css
@@ -7,6 +7,14 @@
text-wrap: auto;
}
+.zero-state-container {
+ padding: var(--rs-space-10);
+}
+
+.zero-state {
+ padding-top: var(--rs-space-17);
+}
+
.table {
width: 100%;
table-layout: fixed;
diff --git a/web/sdk/admin/views/organizations/list/list.module.css b/web/sdk/admin/views/organizations/list/list.module.css
index 8306a41b5..086bc7d25 100644
--- a/web/sdk/admin/views/organizations/list/list.module.css
+++ b/web/sdk/admin/views/organizations/list/list.module.css
@@ -5,7 +5,7 @@
display: flex;
align-items: center;
justify-content: space-between;
- min-height: var(--rs-space-12);
+ height: var(--rs-space-11);
}
.table {
@@ -83,4 +83,4 @@
.select-content {
max-height: 400px;
-}
+}
\ No newline at end of file
diff --git a/web/sdk/admin/views/organizations/list/navbar.tsx b/web/sdk/admin/views/organizations/list/navbar.tsx
index 400aec865..b451f6869 100644
--- a/web/sdk/admin/views/organizations/list/navbar.tsx
+++ b/web/sdk/admin/views/organizations/list/navbar.tsx
@@ -64,16 +64,20 @@ export const OrganizationsNavabar = ({
- }
- data-test-id="admin-create-organization-btn"
- onClick={openCreatePanel}
- >
- New {t.organization({ case: "capital" })}
-
-
+ {!showSearch ? (
+ <>
+ }
+ data-test-id="admin-create-organization-btn"
+ onClick={openCreatePanel}
+ >
+ New {t.organization({ case: "capital" })}
+
+
+ >
+ ) : null}
{showSearch ? (