Skip to content
7 changes: 7 additions & 0 deletions web/sdk/admin/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions web/sdk/admin/views/organizations/details/apis/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ const NoCredentials = () => {
);
};

const ZeroState = () => {
return (
<div className={styles["zero-state-container"]}>
<EmptyState
variant="empty2"
className={styles["zero-state"]}
icon={<InfoCircledIcon />}
heading="API Credentials"
subHeading="Service accounts provide programmatic API access on behalf of this organization."
/>
</div>
);
};

const ErrorState = () => {
return (
<EmptyState
Expand Down Expand Up @@ -191,6 +205,7 @@ export function OrganizationApisView() {
<DataTable.Toolbar />
<DataTable.Content
emptyState={isError ? <ErrorState /> : <NoCredentials />}
zeroState={<ZeroState />}
classNames={{
root: styles["table-wrapper"],
table: styles["table"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export const getColumns = ({
cell: ({ getValue }) => {
const value = getValue() as string;
return (
<Link href={value} target="_blank" data-test-id="invoice-link">
<Link
href={value}
target="_blank"
className={styles["invoice-link"]}
data-test-id="invoice-link"
>
View Invoice
</Link>
);
Expand Down
15 changes: 15 additions & 0 deletions web/sdk/admin/views/organizations/details/invoices/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ const NoInvoices = () => {
);
};

const ZeroState = () => {
return (
<div className={styles["zero-state-container"]}>
<EmptyState
variant="empty2"
className={styles["zero-state"]}
icon={<FileTextIcon />}
heading="Invoices"
subHeading="Invoices generated for this organization's billing activity will appear here."
/>
</div>
);
};

const ErrorState = () => {
const t = useTerminology();
return (
Expand Down Expand Up @@ -180,6 +194,7 @@ export function OrganizationInvoicesView() {
<DataTable.Toolbar />
<DataTable.Content
emptyState={isError ? <ErrorState /> : <NoInvoices />}
zeroState={<ZeroState />}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,3 +34,7 @@
.first-column {
padding-left: var(--rs-space-7);
}

.invoice-link {
text-decoration: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<string, string>,
Expand Down
15 changes: 15 additions & 0 deletions web/sdk/admin/views/organizations/details/members/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ const NoMembers = () => {
);
};

const ZeroState = () => {
return (
<div className={styles["zero-state-container"]}>
<EmptyState
variant="empty2"
className={styles["zero-state"]}
icon={<UsersIcon />}
heading="Members"
subHeading="Members are users who belong to this organization and can access its resources."
/>
</div>
);
};

const ErrorState = () => {
return (
<EmptyState
Expand Down Expand Up @@ -217,6 +231,7 @@ export function OrganizationMembersView() {
<DataTable.Toolbar />
<DataTable.Content
emptyState={isError ? <ErrorState /> : <NoMembers />}
zeroState={<ZeroState />}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export function PatDetailsDialog({
<Dialog.Content className={styles["details-dialog"]}>
<Dialog.Header>
<Dialog.Title>{pat?.title || ""}</Dialog.Title>
<Dialog.CloseButton data-test-id="frontier-sdk-pat-details-dialog-close-btn" />
</Dialog.Header>
<Dialog.Body className={styles["dialog-body"]}>
<Tabs defaultValue="projects" className={styles["tab-root"]}>
Expand Down
25 changes: 21 additions & 4 deletions web/sdk/admin/views/organizations/details/projects/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Flex,
Text,
Menu,
IconButton,
} from "@raystack/apsara";
import type { DataTableColumnDef } from "@raystack/apsara";
import type {
Expand Down Expand Up @@ -45,13 +46,15 @@ interface AddMemberDropdownProps {
eligibleMembers: User[];
isLoading: boolean;
setSearchQuery: (query: string) => void;
disabled?: boolean;
}

function AddMemberDropdown({
onAddMember,
eligibleMembers,
isLoading,
setSearchQuery,
disabled,
}: AddMemberDropdownProps) {
const t = useTerminology();
return (
Expand All @@ -60,7 +63,7 @@ function AddMemberDropdown({
autocompleteMode="manual"
onInputValueChange={setSearchQuery}
>
<Menu.SubmenuTrigger data-test-id="add-members">
<Menu.SubmenuTrigger disabled={disabled} data-test-id="add-members">
Add {t.member({ case: "lower" })}
</Menu.SubmenuTrigger>
<Menu.SubmenuContent>
Expand Down Expand Up @@ -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<HTMLDivElement>) => {
Expand Down Expand Up @@ -130,6 +135,7 @@ function ProjectActionsContent({
eligibleMembers={eligibleMembers}
isLoading={isLoading}
setSearchQuery={setSearchQuery}
disabled={!canAddMember}
/>
<Menu.Item
onClick={handleRenameOptionClick}
Expand All @@ -144,17 +150,19 @@ function ProjectActionsContent({
function ProjectActions({
project,
handleProjectUpdate,
canAddMember,
}: {
project: SearchOrganizationProjectsResponse_OrganizationProject;
handleProjectUpdate: (
project: SearchOrganizationProjectsResponse_OrganizationProject,
) => void;
canAddMember: boolean;
}) {
const [open, setOpen] = useState(false);

const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);

const preventClickBubbling = (e: React.MouseEvent<SVGElement>) => {
const preventClickBubbling = (e: React.MouseEvent) => {
e.stopPropagation();
};

Expand Down Expand Up @@ -182,17 +190,21 @@ function ProjectActions({
<Menu open={open} onOpenChange={handleOpen}>
<Menu.Trigger
render={
<DotsHorizontalIcon
<IconButton
size={3}
onClick={preventClickBubbling}
data-test-id="admin-project-actions"
/>
>
<DotsHorizontalIcon />
</IconButton>
}
/>
<Menu.Content className={styles["table-action-dropdown"]}>
<ProjectActionsContent
project={project}
handleProjectUpdate={handleProjectUpdate}
handleRenameOptionOpen={handleRenameOptionOpen}
canAddMember={canAddMember}
/>
</Menu.Content>
</Menu>
Expand All @@ -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",
Expand Down Expand Up @@ -289,6 +305,7 @@ export const getColumns = ({
<ProjectActions
project={row?.original}
handleProjectUpdate={handleProjectUpdate}
canAddMember={canAddMember}
/>
);
},
Expand Down
19 changes: 19 additions & 0 deletions web/sdk/admin/views/organizations/details/projects/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ const NoProjects = () => {
);
};

const ZeroState = () => {
const t = useTerminology();
return (
<div className={styles["zero-state-container"]}>
<EmptyState
variant="empty2"
className={styles["zero-state"]}
icon={<FileIcon />}
heading={t.project({ plural: true, case: "capital" })}
subHeading={`${t.project({ plural: true, case: "capital" })} group resources and members within this ${t.organization({ case: "lower" })}.`}
/>
</div>
);
};

const ErrorState = () => {
const t = useTerminology();
return (
Expand Down Expand Up @@ -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 ? (
<ProjectMembersDialog
projectId={memberDialogConfig.projectId}
onClose={handleMemberDialogClose}
canAddMember={canAddMember}
/>
) : null}
<Flex justify="center" className={styles["container"]}>
Expand All @@ -192,6 +210,7 @@ export function OrganizationProjectsView() {
<DataTable.Toolbar />
<DataTable.Content
emptyState={isError ? <ErrorState /> : <NoProjects />}
zeroState={<ZeroState />}
classNames={{
table: styles["table"],
root: styles["table-wrapper"],
Expand Down
Loading
Loading