-
Notifications
You must be signed in to change notification settings - Fork 10
feat: enhance unclaimed nodes modal with GitHub repository info fetching #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
cc10df9
34451f1
fe694aa
e5b3cce
5133d2d
8d38aff
1bb3e62
3d3d041
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import type { Meta, StoryObj } from '@storybook/nextjs-vite' | ||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||
| import { useState } from 'react' | ||
| import { AdminCreateNodeFormModal } from './AdminCreateNodeFormModal' | ||
|
|
||
| const meta = { | ||
| title: 'Components/Nodes/AdminCreateNodeFormModal', | ||
| component: AdminCreateNodeFormModal, | ||
| parameters: { | ||
| layout: 'centered', | ||
| docs: { | ||
| description: { | ||
| component: ` | ||
| The AdminCreateNodeFormModal is used by administrators to add unclaimed nodes to the registry. | ||
| It features a repository URL input at the top with a "Fetch Info" button that automatically | ||
| populates form fields from the pyproject.toml file in GitHub repositories. | ||
|
|
||
| ## Features | ||
| - Repository URL input with auto-fetch functionality | ||
| - Form validation using Zod schema | ||
| - Duplicate node detection | ||
| - Integration with React Hook Form | ||
| - Toast notifications for success/error states | ||
| `, | ||
| }, | ||
| }, | ||
| }, | ||
| decorators: [ | ||
| (Story) => { | ||
| const queryClient = new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| retry: false, | ||
| }, | ||
| }, | ||
| }) | ||
| return ( | ||
| <QueryClientProvider client={queryClient}> | ||
| <div style={{ minHeight: '600px' }}> | ||
| <Story /> | ||
| </div> | ||
| </QueryClientProvider> | ||
| ) | ||
| }, | ||
| ], | ||
| tags: ['autodocs'], | ||
| } satisfies Meta<typeof AdminCreateNodeFormModal> | ||
|
|
||
| export default meta | ||
| type Story = StoryObj<typeof meta> | ||
|
|
||
| // Story wrapper component to handle modal state | ||
| function ModalWrapper( | ||
| args: React.ComponentProps<typeof AdminCreateNodeFormModal> | ||
| ) { | ||
| const [open, setOpen] = useState(true) | ||
|
|
||
| return ( | ||
| <AdminCreateNodeFormModal | ||
| {...args} | ||
| open={open} | ||
| onClose={() => { | ||
| console.log('Modal closed') | ||
| setOpen(false) | ||
| // Reopen after a delay for demo purposes | ||
| setTimeout(() => setOpen(true), 1000) | ||
| }} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| export const Default: Story = { | ||
| render: (args) => <ModalWrapper {...args} />, | ||
| args: { | ||
| open: true, | ||
| onClose: () => console.log('onClose'), | ||
| }, | ||
| } | ||
|
|
||
| export const WithGitHubRepo: Story = { | ||
| render: (args) => <ModalWrapper {...args} />, | ||
| args: { | ||
| open: true, | ||
| onClose: () => console.log('onClose'), | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: ` | ||
| This story demonstrates the modal with a GitHub repository URL pre-filled. | ||
| In a real scenario, clicking "Fetch Info" would populate the form fields | ||
| with data from the repository's pyproject.toml file. | ||
| `, | ||
| }, | ||
| }, | ||
| }, | ||
| play: async ({ canvasElement }) => { | ||
| // You could add play interactions here to demonstrate the functionality | ||
| // For example, filling in the repository field and clicking fetch | ||
| }, | ||
| } | ||
|
|
||
| export const Closed: Story = { | ||
| render: () => ( | ||
| <AdminCreateNodeFormModal | ||
| open={false} | ||
| onClose={() => console.log('onClose')} | ||
| /> | ||
| ), | ||
| args: { | ||
| open: false, | ||
| onClose: () => console.log('onClose'), | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: 'The modal in its closed state.', | ||
| }, | ||
| }, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,9 +3,11 @@ import { useQueryClient } from '@tanstack/react-query' | |||||||||||||||||||||||
| import { AxiosError } from 'axios' | ||||||||||||||||||||||||
| import { Button, Label, Modal, Textarea, TextInput } from 'flowbite-react' | ||||||||||||||||||||||||
| import { useRouter } from 'next/router' | ||||||||||||||||||||||||
| import { useState } from 'react' | ||||||||||||||||||||||||
| import { useForm } from 'react-hook-form' | ||||||||||||||||||||||||
| import { HiPlus } from 'react-icons/hi' | ||||||||||||||||||||||||
| import { HiDownload, HiPlus } from 'react-icons/hi' | ||||||||||||||||||||||||
| import { toast } from 'react-toastify' | ||||||||||||||||||||||||
| import TOML from 'smol-toml' | ||||||||||||||||||||||||
| import { customThemeTModal } from 'utils/comfyTheme' | ||||||||||||||||||||||||
| import { z } from 'zod' | ||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
|
|
@@ -47,6 +49,104 @@ const adminCreateNodeDefaultValues: Partial< | |||||||||||||||||||||||
| license: '{file="LICENSE"}', | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface PyProjectData { | ||||||||||||||||||||||||
| name?: string | ||||||||||||||||||||||||
| description?: string | ||||||||||||||||||||||||
| author?: string | ||||||||||||||||||||||||
| license?: string | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async function fetchGitHubRepoInfo( | ||||||||||||||||||||||||
| repoUrl: string | ||||||||||||||||||||||||
| ): Promise<PyProjectData | null> { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| // Parse GitHub URL to extract owner and repo | ||||||||||||||||||||||||
| const urlMatch = repoUrl.match( | ||||||||||||||||||||||||
| /github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/|$)/ | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if (!urlMatch) { | ||||||||||||||||||||||||
| throw new Error('Invalid GitHub URL format') | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const [, owner, repo] = urlMatch | ||||||||||||||||||||||||
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/pyproject.toml` | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const response = await fetch(apiUrl) | ||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||
| if (response.status === 404) { | ||||||||||||||||||||||||
| throw new Error('pyproject.toml not found in repository') | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
snomiao marked this conversation as resolved.
|
||||||||||||||||||||||||
| throw new Error(`GitHub API error: ${response.statusText}`) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const data = await response.json() | ||||||||||||||||||||||||
| // Validate encoding and base64 content | ||||||||||||||||||||||||
| if (data.encoding !== 'base64') { | ||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||
| `Unexpected encoding for pyproject.toml: ${data.encoding}` | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| // Basic base64 validation regex (allows padding) | ||||||||||||||||||||||||
| const base64Regex = | ||||||||||||||||||||||||
| /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ | ||||||||||||||||||||||||
| if (!base64Regex.test(data.content)) { | ||||||||||||||||||||||||
| throw new Error('Invalid base64 content in pyproject.toml') | ||||||||||||||||||||||||
|
Comment on lines
+100
to
+106
|
||||||||||||||||||||||||
| // Strip whitespace from base64 content before validation | |
| const cleanedContent = data.content.replace(/\s/g, '') | |
| // Basic base64 validation regex (allows padding) | |
| const base64Regex = | |
| /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ | |
| if (!base64Regex.test(cleanedContent)) { | |
| throw new Error('Invalid base64 content in pyproject.toml') | |
| // Strip whitespace from base64 content before decoding | |
| const cleanedContent = data.content.replace(/\s/g, '') | |
| if (!cleanedContent) { | |
| throw new Error('Empty content in pyproject.toml') |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The license extraction logic handles license.text but according to PEP 621 specification, the license field can also have a file property (e.g., license = {file = "LICENSE"}). Consider adding support for this format:
} else if (
typeof project.license === 'object' &&
project.license !== null
) {
const license = project.license as any
if (typeof license.text === 'string') {
result.license = license.text
} else if (typeof license.file === 'string') {
result.license = `{file="${license.file}"}`
}
}This would maintain consistency with the default value {file="LICENSE"} defined in the form.
| } | |
| } | |
| } else if (typeof license.file === 'string') { | |
| result.license = `{file="${license.file}"}` | |
| } |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function should handle the edge case where pyproject.toml exists but doesn't contain a [project] section. Currently, if the file is parsed successfully but has no [project] section, the function returns an empty object and shows a success toast, which could be misleading to users.
Consider adding a check after parsing to ensure at least one field was extracted, or provide a more informative message when no project metadata is found:
if (Object.keys(result).length === 0) {
throw new Error('No project metadata found in pyproject.toml')
}| if (Object.keys(result).length === 0) { | |
| throw new Error('No project metadata found in pyproject.toml') | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
fetchGitHubRepoInfofunction is missing JSDoc documentation. As this is a key feature of the PR, it should be well-documented with parameter descriptions, return value explanation, and potential errors that can be thrown. Consider adding: