Browser Sandbox Environment
⚡️ Ready to unleash?
Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.
create-relay-nodes-component
Unlock efficient data display with Relay nodes. This AI agent skill generates reusable components for Backend.AI, supporting GraphQL, sorting, and...
Create Relay Nodes Component Skill
Purpose
This skill generates reusable Relay-based Nodes components that:
- Follow established patterns from BAISchedulingHistoryNodes, BAIRouteNodes, BAISessionHistorySubStepNodes
- Integrate seamlessly with BAITable for data display
- Use Relay fragments for efficient GraphQL data fetching
- Support column customization via
customizeColumnspattern - Include sorting with
disableSortertoggle and table features out of the box - Provide complete starting templates with TODOs for customization
When to Use
Activate this skill when users ask to:
- Create a new Nodes component for displaying GraphQL data
- Generate a table component using Relay fragments
- Build a list view component with Backend.AI UI patterns
- Create reusable data display components with sorting/filtering
Required Information
Minimal User Input
1. GraphQL Type Name (Required)
- Examples:
UserNode,ComputeSessionNode,SessionSchedulingHistory - This determines all other naming and structure
2. Component Location (Optional - has smart defaults)
- Default for
*Nodetypes:packages/backend.ai-ui/src/components/ - Default for other types:
packages/backend.ai-ui/src/components/fragments/ - User can override if needed
Auto-Generated Details
The skill automatically determines:
- Component name:
UserNode→BAIUserNodes - Entity name:
UserNode→User - Fragment prop name:
usersFrgmt - Import paths based on location
- Basic column structure
Implementation Steps
Step 1: Ask User for GraphQL Type
Use AskUserQuestion to get the GraphQL type name:
typescript1{ 2 questions: [ 3 { 4 question: "What is the GraphQL type name for this component?", 5 header: "GraphQL Type", 6 options: [ 7 { 8 label: "UserNode", 9 description: "For User entity list" 10 }, 11 { 12 label: "ComputeSessionNode", 13 description: "For Session entity list" 14 }, 15 { 16 label: "SessionSchedulingHistory", 17 description: "For connection-type entities" 18 }, 19 { 20 label: "Other", 21 description: "Specify custom GraphQL type name" 22 } 23 ], 24 multiSelect: false 25 } 26 ] 27}
If user selects "Other", prompt for the custom type name.
Step 2: Determine Component Location
Ask about location only if needed:
typescript1{ 2 questions: [ 3 { 4 question: "Where should the component be created?", 5 header: "Location", 6 options: [ 7 { 8 label: "packages/backend.ai-ui/src/components/ (Recommended for Node types)", 9 description: "Default location for *Node components" 10 }, 11 { 12 label: "packages/backend.ai-ui/src/components/fragments/", 13 description: "For *Connection or fragment-specific components" 14 }, 15 { 16 label: "react/src/components/", 17 description: "For React-specific (non-shared) components" 18 } 19 ], 20 multiSelect: false 21 } 22 ] 23}
Step 3: Generate Component Name and Variables
Based on GraphQL type, auto-generate:
typescript1// Example transformations: 2// UserNode → BAIUserNodes, User, usersFrgmt 3// ComputeSessionNode → BAIComputeSessionNodes, Session, sessionsFrgmt 4// Route → BAIRouteNodes, Route, routesFrgmt 5 6function generateComponentDetails(graphqlType: string) { 7 // Remove "Node" or "Connection" suffix 8 const cleanName = graphqlType 9 .replace(/Node$/, '') 10 .replace(/Connection$/, ''); 11 12 // Generate component name with BAI prefix and Nodes suffix 13 const componentName = `BAI${cleanName}Nodes`; 14 15 // Extract entity name (e.g., "User" from "UserNode") 16 const entityName = cleanName.replace(/^.*(?=[A-Z])/, ''); 17 18 // Generate fragment prop name (lowercase + Frgmt suffix) 19 const entityLowercase = entityName.toLowerCase(); 20 const fragmentProp = `${entityLowercase}${entityLowercase.endsWith('s') ? '' : 's'}Frgmt`; 21 22 return { 23 componentName, 24 entityName, 25 fragmentProp, 26 graphqlType 27 }; 28}
Step 4: Generate Component File
Create complete TypeScript file with this structure:
CRITICAL PATTERNS (must follow):
-
Column keys must be camelCase —
'createdAt','status', NOT'CREATED_AT'. The query orchestrator usesconvertToOrderBy()fromreact/src/helper/index.tsxto convert camelCase to{ field: 'CREATED_AT', direction: 'ASC' }for Strawberry queries. -
Use
filterOutEmpty+_.mapwithdisableSorter— notsatisfies. This enables runtime sorter toggling. -
Never hardcode
pagination={false}— let the consumer control pagination via...tableProps. -
Callback props for domain-specific interactions — use
onClickXxxcallbacks instead of embedding navigation/modal logic. The consumer wires these up. -
Use
'use memo'directive at the top of the component body for React Compiler optimization.
typescript1import { 2 {ComponentName}Fragment$data, 3 {ComponentName}Fragment$key, 4} from '../../__generated__/{ComponentName}Fragment.graphql'; 5import { filterOutEmpty, filterOutNullAndUndefined } from '../../helper'; 6import { 7 BAIColumnsType, 8 BAIColumnType, 9 BAITable, 10 BAITableProps, 11} from '../Table'; 12import _ from 'lodash'; 13import { useTranslation } from 'react-i18next'; 14import { graphql, useFragment } from 'react-relay'; 15 16// ============================================================================= 17// Type Definitions 18// ============================================================================= 19 20export type {Entity}InList = NonNullable<{ComponentName}Fragment$data[number]>; 21 22// Sorter keys must be camelCase — convertToOrderBy() handles UPPER_SNAKE_CASE conversion 23const available{Entity}SorterKeys = [ 24 'createdAt', 25 // TODO: Add more sortable fields in camelCase 26] as const; 27 28export const available{Entity}SorterValues = [ 29 ...available{Entity}SorterKeys, 30 ...available{Entity}SorterKeys.map((key) => `-${key}` as const), 31] as const; 32 33const isEnableSorter = (key: string) => { 34 return _.includes(available{Entity}SorterKeys, key); 35}; 36 37// ============================================================================= 38// Props Interface 39// ============================================================================= 40 41export interface {ComponentName}Props 42 extends Omit< 43 BAITableProps<{Entity}InList>, 44 'dataSource' | 'columns' | 'onChangeOrder' 45 > { 46 {fragmentProp}: {ComponentName}Fragment$key; 47 customizeColumns?: ( 48 baseColumns: BAIColumnsType<{Entity}InList>, 49 ) => BAIColumnsType<{Entity}InList>; 50 disableSorter?: boolean; 51 onChangeOrder?: ( 52 order: (typeof available{Entity}SorterValues)[number] | null, 53 ) => void; 54 // TODO: Add domain-specific callback props (e.g., onClickSessionId, onClickErrorData) 55} 56 57// ============================================================================= 58// Component 59// ============================================================================= 60 61const {ComponentName} = ({ 62 {fragmentProp}, 63 customizeColumns, 64 disableSorter, 65 onChangeOrder, 66 ...tableProps 67}: {ComponentName}Props) => { 68 'use memo'; 69 const { t } = useTranslation(); 70 71 // TODO: Customize fragment fields based on your GraphQL schema 72 const data = useFragment<{ComponentName}Fragment$key>( 73 graphql` 74 fragment {ComponentName}Fragment on {GraphQLType} @relay(plural: true) { 75 id @required(action: NONE) 76 # TODO: Add fields you need from the GraphQL type 77 } 78 `, 79 {fragmentProp}, 80 ); 81 82 // ============================================================================= 83 // Column Definitions — keys must be camelCase 84 // ============================================================================= 85 86 const baseColumns = _.map( 87 filterOutEmpty<BAIColumnType<{Entity}InList>>([ 88 { 89 key: 'id', 90 title: 'ID', 91 dataIndex: 'id', 92 fixed: 'left', 93 }, 94 // TODO: Add more columns with camelCase keys 95 // { 96 // key: 'createdAt', 97 // title: t('comp:{ComponentName}.CreatedAt'), 98 // dataIndex: 'createdAt', 99 // sorter: isEnableSorter('createdAt'), 100 // render: (value) => dayjs(value).format('ll LT'), 101 // }, 102 ]), 103 (column) => { 104 return disableSorter ? _.omit(column, 'sorter') : column; 105 }, 106 ); 107 108 const allColumns = customizeColumns 109 ? customizeColumns(baseColumns) 110 : baseColumns; 111 112 return ( 113 <BAITable 114 rowKey={'id'} 115 dataSource={filterOutNullAndUndefined(data)} 116 columns={allColumns} 117 scroll={{ x: 'max-content' }} 118 onChangeOrder={(order) => { 119 onChangeOrder?.( 120 (order as (typeof available{Entity}SorterValues)[number]) || null, 121 ); 122 }} 123 {...tableProps} 124 /> 125 ); 126}; 127 128export default {ComponentName};
Step 5: Query Orchestrator Pattern
When integrating into a page, follow this pattern for pagination/order that avoids full-page suspense on pagination changes:
typescript1// In the query orchestrator (page component): 2import { useDeferredValue, useState } from 'react'; 3import { convertToOrderBy } from '../helper'; 4 5// Simple state — no need for useBAIPaginationOptionState unless URL persistence is needed 6const [routePagination, setRoutePagination] = useState({ current: 1, pageSize: 10 }); 7const [routeOrder, setRouteOrder] = useState<string | null>(null); 8 9// useDeferredValue keeps previous UI visible while new data loads 10const deferredPagination = useDeferredValue(routePagination); 11const deferredOrder = useDeferredValue(routeOrder); 12 13// In query variables: 14const { data } = useLazyLoadQuery(query, { 15 // ... other vars 16 routeFirst: deferredPagination.pageSize, 17 routeOffset: (deferredPagination.current - 1) * deferredPagination.pageSize, 18 // convertToOrderBy converts camelCase 'createdAt' → { field: 'CREATED_AT', direction: 'ASC' } 19 routeOrderBy: convertToOrderBy(deferredOrder) ?? undefined, 20}); 21 22// In JSX — pagination.onChange wires directly to state setter: 23<BAIRouteNodes 24 routesFrgmt={...} 25 order={routeOrder} 26 onChangeOrder={setRouteOrder} 27 pagination={{ 28 ...routePagination, 29 total: data?.routes?.count, 30 showSizeChanger: true, 31 onChange: (page, pageSize) => { 32 setRoutePagination({ current: page, pageSize }); 33 }, 34 }} 35/>
Key points:
useDeferredValuewraps pagination/order state so React shows stale data while loadingconvertToOrderBy(camelCaseString)converts to{ field: 'UPPER_SNAKE', direction }for Strawberrypaginationis passed via...tableProps— never hardcodepagination={false}in the Nodes component- For queries co-located in a parent query, use
@skipOnClient(if: $skip)for feature-gated fields
Step 6: Provide Next Steps to User
After generation, show comprehensive next steps:
markdown1Component generated successfully! 2 3**Generated File:** 4`{full_path_to_generated_file}` 5 6**Next Steps:** 7 81. **Run Relay Compiler** to generate fragment types: 9 ```bash 10 pnpm run relay 11 ``` 12 132. **Customize GraphQL Fragment:** 14 - Add fields you need from {GraphQLType} 15 - Consider performance: only request fields you'll display 16 173. **Define Table Columns:** 18 - Customize the `baseColumns` array 19 - Use **camelCase** keys that match fragment field names 20 - Add callback props (`onClickXxx`) for interactive columns 21 224. **Update Sortable Fields:** 23 - Edit `available{Entity}SorterKeys` with camelCase field names 24 - Only include fields that support sorting in your API 25 265. **Add Internationalization:** 27 - Add translation keys to locale files 28 296. **Export from barrel:** 30 - Add export to the appropriate `index.ts` 31 327. **Verify:** 33 ```bash 34 bash scripts/verify.sh 35 ```
Architecture Pattern
Generated components follow the Relay Fragment Architecture:
┌─────────────────────────────────────┐
│ Query Orchestrator Component │
│ - useLazyLoadQuery │
│ - useState for pagination/order │
│ - useDeferredValue for smoothness │
│ - convertToOrderBy for Strawberry │
│ - Passes fragment refs │
└───────────────┬─────────────────────┘
│ fragment ref
▼
┌─────────────────────────────────────┐
│ Nodes Component (Generated) │
│ - useFragment │
│ - Receives fragment ref as prop │
│ - baseColumns + customizeColumns │
│ - disableSorter toggle │
│ - onClickXxx callback props │
│ - Renders BAITable │
└─────────────────────────────────────┘
Benefits:
- Separation of data fetching from presentation
- Reusability across different queries
- Type-safe with Relay-generated types
- Colocated fragments with components
- Flexible via
customizeColumnspattern
Common Customization Patterns
Adding Custom Actions Column
typescript1customizeColumns={(baseColumns) => [ 2 ...baseColumns, 3 { 4 key: 'actions', 5 title: 'Actions', 6 fixed: 'right', 7 render: (__, record) => ( 8 <BAIButton size="small" onClick={() => handleEdit(record)}> 9 Edit 10 </BAIButton> 11 ), 12 }, 13]}
Filtering Columns
typescript1customizeColumns={(baseColumns) => 2 baseColumns.filter((col) => col.key !== 'unwanted_column') 3}
Reordering Columns
typescript1customizeColumns={(baseColumns) => { 2 const nameCol = baseColumns.find((col) => col.key === 'name'); 3 const others = baseColumns.filter((col) => col.key !== 'name'); 4 return nameCol ? [nameCol, ...others] : others; 5}}
Best Practices
-
Column Keys
- Always use camelCase keys matching GraphQL field names
convertToOrderBy()handles conversion toUPPER_SNAKE_CASEfor StrawberryOrderByinputs- Never use UPPER_SNAKE_CASE in column keys — that breaks
BAITableorder matching
-
Fragment Fields
- Only request fields you'll actually display
- Use
@required(action: NONE)for critical fields - Consider nested fragments for related data
-
Pagination
- Never hardcode
pagination={false}in the Nodes component - Let the consumer pass pagination via
...tableProps - Use
useDeferredValuein the query orchestrator for smooth transitions
- Never hardcode
-
Sorting
- Use
disableSorterprop to conditionally disable all sorters filterOutEmpty+_.mappattern handles sorter removal- Keep
availableSorterKeysin sync with API capabilities
- Use
-
Callback Props
- Use
onClickXxxcallbacks for domain-specific interactions (navigation, modals) - Don't embed navigation logic inside the Nodes component — keep it presentation-only
- Use
-
Type Safety
- Always run Relay compiler after fragment changes
- Use generated
$keyand$datatypes - Export type definitions for reuse
-
React Compiler
- Always include
'use memo'directive at the top of the component body
- Always include
Reference Files
| File | Purpose |
|---|---|
BAISchedulingHistoryNodes.tsx | Template with customizeColumns, disableSorter, expandable rows |
BAISessionHistorySubStepNodes.tsx | Minimal template with customizeColumns |
BAIRouteNodes.tsx | Template with callback props (onClickSessionId, onClickErrorData) |
BAIAgentTable.tsx | Complex example with many columns |
Location: packages/backend.ai-ui/src/components/fragments/
Notes
- Generated components are starting templates requiring customization
- Components compile after running
pnpm run relay - Follow
customizeColumnspattern for flexibility - Always test with real GraphQL data before finalizing
- Use
bash scripts/verify.shto validate Relay, Lint, Format, and TypeScript
FAQ & Installation Steps
These questions and steps mirror the structured data on this page for better search understanding.
? Frequently Asked Questions
What is create-relay-nodes-component?
create-relay-nodes-component is a skill that generates reusable Relay-based nodes components for efficient GraphQL data fetching and display.
How do I install create-relay-nodes-component?
Run the command: npx killer-skills add lablup/backend.ai-webui/create-relay-nodes-component. It works with Cursor, Windsurf, VS Code, Claude Code, and 19+ other IDEs.
Which IDEs are compatible with create-relay-nodes-component?
This skill is compatible with Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer. Use the Killer-Skills CLI for universal one-command installation.
↓ How To Install
-
1. Open your terminal
Open the terminal or command line in your project directory.
-
2. Run the install command
Run: npx killer-skills add lablup/backend.ai-webui/create-relay-nodes-component. The CLI will automatically detect your IDE or AI agent and configure the skill.
-
3. Start using the skill
The skill is now active. Your AI agent can use create-relay-nodes-component immediately in the current project.