⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
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
3 changes: 0 additions & 3 deletions web/apps/admin/.eslintrc.cjs

This file was deleted.

2 changes: 1 addition & 1 deletion web/apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@radix-ui/react-form": "^0.0.2",
"@radix-ui/react-icons": "^1.3.0",
"@raystack/apsara": "^0.53.2",
"@raystack/frontier": "^0.78.0",
"@raystack/frontier": "workspace:^",
"@raystack/proton": "0.1.0-b1687af73f994fa9612a023c850aa97c35735af8",
"@stitches/react": "^1.2.8",
"@tanstack/react-query": "^5.83.0",
Expand Down
15 changes: 15 additions & 0 deletions web/apps/admin/src/pages/roles/RolesPage.tsx
Copy link
Member

Choose a reason for hiding this comment

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

Just name it RolesPage

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RolesView } from "@raystack/frontier/admin";
import { useParams, useNavigate } from "react-router-dom";

export function RolesPage() {
const { roleId } = useParams();
const navigate = useNavigate();

return (
<RolesView
selectedRoleId={roleId}
onSelectRole={(id) => navigate(`/roles/${encodeURIComponent(id)}`)}
onCloseDetail={() => navigate("/roles")}
/>
);
}
9 changes: 4 additions & 5 deletions web/apps/admin/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import ProductList from "./containers/products.list";
import ProductDetails from "./containers/products.list/details";
import ProductPrices from "./containers/products.list/prices";

import Roles from "./containers/roles.list";
import RoleDetails from "./containers/roles.list/details";
import { RolesPage } from "./pages/roles/RolesPage";

import { AppContext } from "./contexts/App";
import { SuperAdminList } from "./containers/super_admins/list";
Expand Down Expand Up @@ -90,9 +89,9 @@ export default memo(function AppRoutes() {
<Route path=":planId" element={<PlanDetails />} />
</Route>

<Route path="roles" element={<Roles />}>
<Route path=":roleId" element={<RoleDetails />} />
</Route>
<Route path="roles" element={<RolesPage />} />
<Route path="roles/:roleId" element={<RolesPage />} />

<Route path="products" element={<ProductList />}>
<Route path=":productId" element={<ProductDetails />} />
</Route>
Expand Down
4 changes: 3 additions & 1 deletion web/apps/admin/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export const exportCsvFromStream = async <T>(
}
}

const blob = new Blob(chunks, { type: "text/csv" });
// BlobPart is a DOM global type; no-undef doesn't recognize type-only refs
// eslint-disable-next-line no-undef
const blob = new Blob(chunks as BlobPart[], { type: "text/csv" });
downloadFile(blob, filename);
};

Expand Down
41 changes: 41 additions & 0 deletions web/lib/admin/components/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { CSSProperties, PropsWithChildren } from "react";
import { Flex, Text } from "@raystack/apsara";
import styles from "./page-header.module.css";

export type PageHeaderTypes = {
title: string;
breadcrumb: { name: string; href?: string }[];
className?: string;
style?: CSSProperties;
};

export function PageHeader({
title,
breadcrumb,
children,
className,
style = {},
...props
}: PropsWithChildren<PageHeaderTypes>) {
return (
<Flex
align="center"
justify="between"
className={className}
style={{ padding: "16px 24px", ...style }}
Copy link
Member

Choose a reason for hiding this comment

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

can we move add class for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

{...props}
>
<Flex align="center" gap="medium">
<Flex align="center" gap="small" className={styles.breadcrumb}>
<Text style={{ fontSize: "14px", fontWeight: "500" }}>{title}</Text>
Copy link
Member

Choose a reason for hiding this comment

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

fontWeight and fontSize can be props

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

{breadcrumb.map((item) => (
<span key={item.name} className={styles.breadcrumbItem}>
{item.name}
</span>
))}
</Flex>
</Flex>
<Flex gap="small">{children}</Flex>
</Flex>
);
}
19 changes: 19 additions & 0 deletions web/lib/admin/components/PageTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from "react";

interface PageTitleProps {
title?: string;
appName?: string;
}

export function PageTitle({ title, appName }: PageTitleProps) {
const fullTitle = title && appName ? `${title} | ${appName}` : title ?? appName ?? "";

useEffect(() => {
if (fullTitle) document.title = fullTitle;
return () => {
document.title = appName ?? "";
};
}, [fullTitle, appName]);

return null;
}
31 changes: 31 additions & 0 deletions web/lib/admin/components/SheetHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CSSProperties } from "react";
import { Cross1Icon } from "@radix-ui/react-icons";
import { Flex, Text } from "@raystack/apsara";

type SheetHeaderProps = {
title: string;
onClick: () => void;
"data-testid"?: string;
};

export function SheetHeader({ title, onClick, "data-testid": dataTestId }: SheetHeaderProps) {
return (
<Flex justify="between" style={styles.header}>
<Text size={4} style={{ fontWeight: "500" }}>
Copy link
Member

Choose a reason for hiding this comment

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

fontWeight can be props

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

{title}
</Text>
<Cross1Icon

Check warning on line 17 in web/lib/admin/components/SheetHeader.tsx

View workflow job for this annotation

GitHub Actions / JS SDK Lint

Elements with an onClick handler must have a data-test-id attribute
onClick={onClick}
style={{ cursor: "pointer" }}
data-testid={dataTestId ?? "admin-close-btn"}
/>
</Flex>
);
}

const styles: { header: CSSProperties } = {
header: {
padding: "18px 32px",
borderBottom: "1px solid var(--rs-color-border-base-primary)",
},
};
Comment on lines +11 to +31
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the SheetHeader.tsx file
find . -name "SheetHeader.tsx" -type f | head -20

Repository: raystack/frontier

Length of output: 103


🏁 Script executed:

# Search for Cross1Icon component definition
find . -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "Cross1Icon" | head -10

Repository: raystack/frontier

Length of output: 408


🏁 Script executed:

# Look for any accessibility-related attributes or patterns in the file
rg "role|aria-|keyboard" web/lib/admin/components/SheetHeader.tsx -A 3 -B 3

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Read the SheetHeader.tsx file
cat -n web/lib/admin/components/SheetHeader.tsx

Repository: raystack/frontier

Length of output: 1078


🏁 Script executed:

# Search for Cross1Icon import and definition
rg "import.*Cross1Icon|from.*@radix" web/lib/admin/components/SheetHeader.tsx -A 2 -B 2

Repository: raystack/frontier

Length of output: 204


🏁 Script executed:

# Look for Cross1Icon in the entire codebase to understand its source
rg "Cross1Icon" --type tsx --type ts -B 2 | head -50

Repository: raystack/frontier

Length of output: 88


🏁 Script executed:

# Search for other uses of Cross1Icon or similar icon patterns in the codebase
rg "Cross1Icon|Icon.*onClick" -A 3 -B 1 | head -60

Repository: raystack/frontier

Length of output: 4304


🏁 Script executed:

# Look for button patterns with icons in the codebase
rg "<button.*aria-label|type=\"button\".*Icon" -A 2 -B 2 | head -80

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Check if there are accessibility patterns or components for icon buttons
rg "IconButton|close.*button|aria-label" web/lib/admin/components -A 2 -B 2

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Search for IconButton component definition and usage
rg "IconButton" web/ -A 5 -B 2 | head -100

Repository: raystack/frontier

Length of output: 7568


🏁 Script executed:

# Check if `@raystack/apsara` has IconButton documentation or types
find . -name "*.tsx" -o -name "*.ts" | xargs grep -l "export.*IconButton\|from.*apsara.*IconButton" | head -10

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Look for tests related to SheetHeader
find . -name "*SheetHeader*test*" -o -name "*SheetHeader*.spec.*"

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Search for any related accessibility tests or patterns
rg "admin-close-btn|testid.*close" -A 3 -B 3 | head -80

Repository: raystack/frontier

Length of output: 988


🏁 Script executed:

# Check the imports available in SheetHeader.tsx and if IconButton can be imported
rg "from.*@raystack/apsara" web/lib/admin/components/SheetHeader.tsx

Repository: raystack/frontier

Length of output: 107


🏁 Script executed:

# Look at how IconButton is imported in other files to understand the pattern
rg "import.*IconButton.*@raystack/apsara" web/apps/admin/src/ -B 2 | head -30

Repository: raystack/frontier

Length of output: 816


🏁 Script executed:

# Check if SheetHeader component is used in tests and what accessibility expectations exist
find . -path "*/test*" -o -path "*/spec*" | xargs grep -l "SheetHeader\|admin-close-btn" 2>/dev/null

Repository: raystack/frontier

Length of output: 45


🏁 Script executed:

# Check if IconButton can be imported and used in SheetHeader
grep -r "export.*IconButton" web/lib/admin/ 2>/dev/null | head -20

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Look at an actual IconButton usage to understand required props
rg "IconButton" web/apps/admin/src/pages/invoices/list/navbar.tsx -A 5 -B 2 | head -40

Repository: raystack/frontier

Length of output: 638


🏁 Script executed:

# Check if there's a pattern of using native button vs IconButton in headers/close actions
rg "<button.*close|<IconButton.*close" -A 3 -B 3 | head -60

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

# Verify `@raystack/apsara` exports IconButton from web/lib
find web/lib -name "*.ts" -o -name "*.tsx" | xargs grep "export.*IconButton" 2>/dev/null

Repository: raystack/frontier

Length of output: 45


Use IconButton for the close action to meet accessibility requirements.
The Cross1Icon lacks button semantics and an accessible name, making it inaccessible to keyboard users. Use the IconButton component from @raystack/apsara (already available in this file's dependencies and used throughout the codebase) with an aria-label instead of attaching onClick directly to the icon.

♿ Proposed fix
 export function SheetHeader({ title, onClick, "data-testid": dataTestId }: SheetHeaderProps) {
   return (
     <Flex justify="between" style={styles.header}>
       <Text size={4} style={{ fontWeight: "500" }}>
         {title}
       </Text>
-      <Cross1Icon
+      <IconButton
+        aria-label="Close"
         onClick={onClick}
-        style={{ cursor: "pointer" }}
         data-testid={dataTestId ?? "admin-close-btn"}
-      />
+      >
+        <Cross1Icon />
+      </IconButton>
     </Flex>
   );
 }

Also update the import to include IconButton:

-import { Flex, Text } from "@raystack/apsara";
+import { Flex, Text, IconButton } from "@raystack/apsara";
🧰 Tools
🪛 GitHub Check: JS SDK Lint

[warning] 17-17:
Elements with an onClick handler must have a data-test-id attribute

🤖 Prompt for AI Agents
In `@web/lib/admin/components/SheetHeader.tsx` around lines 11 - 31, Replace the
clickable Cross1Icon in SheetHeader with the IconButton component from
`@raystack/apsara` so the close control has proper button semantics and an
accessible name; update the import to include IconButton, render IconButton
(passing the Cross1Icon as its icon prop or children), move the onClick handler
from Cross1Icon to IconButton, add aria-label (e.g., "Close sheet") and keep
data-testid usage (use dataTestId or default "admin-close-btn") to preserve
tests.

42 changes: 42 additions & 0 deletions web/lib/admin/components/page-header.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.breadcrumb {
list-style: none;
padding: 0;
margin: 0;
display: flex;
font-size: 16px;
Copy link
Member

Choose a reason for hiding this comment

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

can we use css variables here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

}

.breadcrumb a {
text-decoration: none;
color: #007bff;
Copy link
Member

Choose a reason for hiding this comment

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

can we use css variables here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

display: inline-block;
margin-right: 5px;
}

.breadcrumb a::before {
content: ">";
color: #666;
Copy link
Member

Choose a reason for hiding this comment

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

same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

}

.breadcrumb a:first-child::before {
content: "";
}

.breadcrumb a:last-child {
margin-right: 0;
}

.breadcrumbItem {
margin-right: 5px;
}

.breadcrumbItem::before {
content: ">";
color: #666;
Copy link
Member

Choose a reason for hiding this comment

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

same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Out of scope for this PR. Will be picked up in another ticket.

margin-right: 5px;
}

.breadcrumbItem:first-of-type::before {
content: "";
margin-right: 0;
}
3 changes: 3 additions & 0 deletions web/lib/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use client";

export { default as RolesView } from "./views/roles";
8 changes: 8 additions & 0 deletions web/lib/admin/utils/connect-timestamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt";

export function timestampToDate(timestamp?: Timestamp): Date | null {
if (!timestamp) return null;
return timestampDate(timestamp);
}

export type TimeStamp = Timestamp;
12 changes: 12 additions & 0 deletions web/lib/admin/utils/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function reduceByKey<T extends Record<string, unknown>>(
data: T[],
key: keyof T
): Record<string, T> {
return data.reduce(
(acc, value) => ({
...acc,
[String(value[key])]: value,
}),
{} as Record<string, T>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export const getColumns: () => DataTableColumnDef<Role, unknown>[] = () => {
classNames: {
cell: styles.permissionsColumn,
},
cell: info => <Flex>{(info.getValue() as string[]).join(", ")}</Flex>,
cell: info => (
<Flex direction="column" gap={1} className={styles.permissionsColumn}>
{((info.getValue() as string[]) || []).map((p, i) => (
<span key={i}>{p}</span>
))}
</Flex>
),
footer: props => props.column.id,
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Flex, Text, Grid } from "@raystack/apsara";
import { useRole } from ".";

export default function RoleDetails() {
const { role } = useRole();
import type { Role } from "@raystack/proton/frontier";

export default function RoleDetails({ role }: { role: Role | null }) {
return (
<Flex direction="column" gap={9}>
<Text size={4}>{role?.name}</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { DataTable } from "@raystack/apsara";
import PageHeader from "~/components/page-header";
import { PageHeader } from "../../components/PageHeader";
import styles from "./roles.module.css";

const pageHeader = {
title: "Roles",
breadcrumb: [],
breadcrumb: [] as { name: string; href?: string }[],
};

export const RolesHeader = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import { EmptyState, Flex, DataTable, Sheet } from "@raystack/apsara";
import {
Outlet,
useNavigate,
useOutletContext,
useParams,
} from "react-router-dom";
import { useCallback, useState } from "react";

import { reduceByKey } from "~/utils/helper";
import { reduceByKey } from "../../utils/helper";
import { getColumns } from "./columns";
import { RolesHeader } from "./header";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import PageTitle from "~/components/page-title";
import { PageTitle } from "../../components/PageTitle";
import styles from "./roles.module.css";
import { SheetHeader } from "~/components/sheet/header";
import { SheetHeader } from "../../components/SheetHeader";
import { FrontierServiceQueries, Role } from "@raystack/proton/frontier";
import { useQuery } from "@connectrpc/connect-query";

type ContextType = { role: Role | null };
export default function RoleList() {
const { roleId } = useParams();
const navigate = useNavigate();
import RoleDetails from "./details";

export type RolesViewProps = {
selectedRoleId?: string;
onSelectRole?: (roleId: string) => void;

Check warning on line 18 in web/lib/admin/views/roles/index.tsx

View workflow job for this annotation

GitHub Actions / JS SDK Lint

'roleId' is defined but never used
onCloseDetail?: () => void;
appName?: string;
};

export default function RolesView({
selectedRoleId: controlledRoleId,
onSelectRole,
onCloseDetail,
appName,
}: RolesViewProps = {}) {
const [internalRoleId, setInternalRoleId] = useState<string | undefined>();

const selectedRoleId = controlledRoleId ?? internalRoleId;
const handleClose = useCallback(
() => (onCloseDetail ? onCloseDetail() : setInternalRoleId(undefined)),
[onCloseDetail]
);
const handleRowClick = useCallback(
(role: Role) =>
onSelectRole ? onSelectRole(role.id ?? "") : setInternalRoleId(role.id ?? undefined),
[onSelectRole]
);
Comment on lines +36 to +40
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent handling of undefined role ID.

Line 38 converts role.id to empty string (role.id ?? ""), while line 38 in the else branch uses role.id ?? undefined. This inconsistency could cause subtle bugs if consumers compare selectedRoleId values.

🐛 Proposed fix for consistency
   const handleRowClick = useCallback(
     (role: Role) =>
-      onSelectRole ? onSelectRole(role.id ?? "") : setInternalRoleId(role.id ?? undefined),
+      onSelectRole ? onSelectRole(role.id ?? "") : setInternalRoleId(role.id),
     [onSelectRole]
   );

Note: role.id is already string | undefined, so ?? undefined is redundant.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleRowClick = useCallback(
(role: Role) =>
onSelectRole ? onSelectRole(role.id ?? "") : setInternalRoleId(role.id ?? undefined),
[onSelectRole]
);
const handleRowClick = useCallback(
(role: Role) =>
onSelectRole ? onSelectRole(role.id ?? "") : setInternalRoleId(role.id),
[onSelectRole]
);
🤖 Prompt for AI Agents
In `@web/lib/admin/views/roles/index.tsx` around lines 36 - 40, handleRowClick
inconsistently maps Role.id to "" in the onSelectRole branch but to undefined in
the setInternalRoleId branch; make both branches pass the same type (prefer
leaving id as-is or using undefined consistently). Update the handleRowClick
callback to call onSelectRole(role.id) (or onSelectRole(role.id ?? undefined) if
callback expects string|undefined) and setInternalRoleId(role.id) so both
branches use role.id (string | undefined) consistently; reference the
handleRowClick function, the onSelectRole prop, setInternalRoleId, and the
Role.id field when making the change.


const {
data: roles = [],
Expand All @@ -35,12 +53,6 @@
);
const roleMapByName = reduceByKey(roles ?? [], "id");

function onClose() {
navigate("/roles");
}
function onRowClick(role: Role) {
navigate(`${encodeURIComponent(role.id ?? "")}`);
}
if (isError) {
console.error("ConnectRPC Error:", error);
return (
Expand All @@ -58,14 +70,14 @@
const columns = getColumns();
return (
<DataTable
onRowClick={onRowClick}
onRowClick={handleRowClick}
data={roles}
columns={columns}
mode="client"
defaultSort={{ name: "title", order: "asc" }}
isLoading={isLoading}>
<Flex direction="column">
<PageTitle title="Roles" />
<PageTitle title="Roles" appName={appName} />
<RolesHeader />
<DataTable.Content
emptyState={noDataChildren}
Expand All @@ -74,18 +86,16 @@
table: styles.table,
}}
/>
<Sheet open={roleId !== undefined}>
<Sheet open={selectedRoleId !== undefined}>
<Sheet.Content className={styles.sheetContent}>
<SheetHeader

Check warning on line 91 in web/lib/admin/views/roles/index.tsx

View workflow job for this annotation

GitHub Actions / JS SDK Lint

Elements with an onClick handler must have a data-test-id attribute
title="Role Details"
onClick={onClose}
data-test-id="role-details-header"
onClick={handleClose}
data-testid="role-details-header"
/>
<Flex className={styles.sheetContentBody}>
<Outlet
context={{
role: roleId ? roleMapByName[roleId] : null,
}}
<RoleDetails
role={selectedRoleId ? roleMapByName[selectedRoleId] ?? null : null}
/>
</Flex>
</Sheet.Content>
Expand All @@ -95,10 +105,6 @@
);
}

export function useRole() {
return useOutletContext<ContextType>();
}

export const noDataChildren = (
<EmptyState icon={<ExclamationTriangleIcon />} heading="0 role created" />
);
Loading
Loading