Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 "~/admin/utils/helper";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize all member-facing role labels.

The table uses formatRoleTitle, but the filter options at Lines 99-101 and assignment actions at Lines 154-167 still render raw role.title. This leaves users seeing inconsistent values such as Owner in the table and Organization Owner elsewhere. Apply the helper to those display labels while preserving the raw title in API payloads.

Also applies to: 47-50


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>
Comment on lines +193 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'web/sdk/admin/views/organizations/details/projects/(columns|members/columns)\.tsx$|IconButton|DotsHorizontalIcon' || true

echo
echo "Target snippets:"
for f in \
  web/sdk/admin/views/organizations/details/projects/columns.tsx \
  web/sdk/admin/views/organizations/details/projects/members/columns.tsx
do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '160,215p' "$f"
    echo "--- $f members"
    sed -n '80,125p' "$f"
  fi
done

echo
echo "IconButton definitions/usages nearby:"
rg -n "function IconButton|const IconButton|IconButton|DotsHorizontalIcon|aria-label|title=" web/sdk/admin | head -200 || true

Repository: raystack/frontier

Length of output: 16491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read-only behavioral/structural probe for icon-only button accessibility indicators.
python3 - <<'PY'
import re, pathlib

xs = {
 "web/sdk/admin/views/organizations/details/projects/columns.tsx": (188, 204),
 "web/sdk/admin/views/organizations/details/projects/members/columns.tsx": (97, 113),
}
for path, (start, end) in xs.items():
    text = pathlib.Path(path).read_text()
    lines = text.splitlines()
    block = "\n".join(lines[start-1:end])
    print(f"--- {path}: lines {start}-{end}")
    print(block)
    print("contains aria-label/aria-labelledby:", bool(re.search(r"aria-(label|labelled-by)\s*=", block)))
    print("contains title:", bool(re.search(r"\btitle\s*=", block)))
    print("contains IconButton:", "IconButton" in block)
    print("contains icon trigger:", bool(re.search(r"<(DotsHorizontalIcon)|<.*Icon", block, re.S)))
PY

Repository: raystack/frontier

Length of output: 1552


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "IconButton packages/usages:"
rg -n 'from "`@raystack/apsara`"|from .*apsara.*|IconButton' package.json web/sdk/admin/package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || true
echo
rg -n 'export .*IconButton|const IconButton|function IconButton|IconButton =|IconButton(' . --glob '!node_modules' --glob '!dist' --glob '!build' | head -300

echo
echo "Imports in target files:"
sed -n '1,30p' web/sdk/admin/views/organizations/details/projects/columns.tsx
sed -n '1,30p' web/sdk/admin/views/organizations/details/projects/members/columns.tsx

Repository: raystack/frontier

Length of output: 397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Imports in target files:"
sed -n '1,30p' web/sdk/admin/views/organizations/details/projects/columns.tsx
sed -n '1,30p' web/sdk/admin/views/organizations/details/projects/members/columns.tsx

echo
echo "IconButton usages in target package paths:"
rg -n 'from "`@raystack/apsara`"|IconButton|title=/aria-label=|data-test-id="admin-project-(actions|member-action-menu)"' web/sdk/admin/views/organizations/details/projects/columns.tsx web/sdk/admin/views/organizations/details/projects/members/columns.tsx

echo
echo "Local IconButton-like component definitions excluding generated/package dirs:"
rg -n 'const IconButton|function IconButton|export .*IconButton|IconButton\s*=' web/sdk . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!*.lock' | head -200

Repository: raystack/frontier

Length of output: 3299


🌐 Web query:

@raystack/apsara IconButton aria-label accessible name icon button documentation

💡 Result:

In the @raystack/apsara component library, the IconButton component is designed specifically for icon-only buttons [1][2]. Because these buttons lack visible text, providing an accessible name is mandatory to ensure assistive technologies can convey the button's purpose to users [3][1]. You must use the aria-label attribute to provide this accessible name [4][1]. Example usage: import { IconButton } from '@raystack/apsara'; import { TrashIcon } from '@radix-ui/react-icons'; function Actions { return ( ); } The documentation for IconButton highlights that this attribute is required for accessibility [1][2]. Additionally, the library advises considering the use of tooltips to complement the aria-label for improved usability [3]. As the IconButton component extends standard HTML button attributes, it also supports other standard ARIA attributes for managing state and behavior [3][1].

Citations:


Add accessible names to both icon-only action triggers.

Both triggers render only DotsHorizontalIcon, so IconButton has no accessible name.

  • web/sdk/admin/views/organizations/details/projects/columns.tsx: add aria-label="Project actions".
  • web/sdk/admin/views/organizations/details/projects/members/columns.tsx: add aria-label="Member actions".
📍 Affects 2 files
  • web/sdk/admin/views/organizations/details/projects/columns.tsx#L193-L199 (this comment)
  • web/sdk/admin/views/organizations/details/projects/members/columns.tsx#L102-L110

}
/>
<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