lblt 1 天之前
父节点
当前提交
a643455c0c

+ 1 - 1
.env.local

@@ -1,2 +1,2 @@
 # Local development environment variables
-VITE_API_URL=http://localhost:8000
+VITE_API_URL=http://localhost:8080

+ 6 - 0
src/App.tsx

@@ -33,6 +33,9 @@ import {
 import {
   AppList,
   AppCreate,
+  AppCreateChoice,
+  AppCreateManual,
+  AppImportCompose,
   AppShow,
 } from "./pages/apps";
 import {
@@ -217,8 +220,11 @@ function App() {
                     <Route path="/apps">
                       <Route index element={<AppList />} />
                       <Route path="create" element={<AppCreate />} />
+                      <Route path="create/manual" element={<AppCreateManual />} />
+                      <Route path="import-compose" element={<AppImportCompose />} />
                       <Route path="show/:id" element={<AppShow />} />
                     </Route>
+                    <Route path="/apps/import-compose" element={<AppImportCompose />} />
                     <Route path="/components">
                       <Route index element={<ComponentList />} />
                       <Route path="create" element={<ComponentCreate />} />

+ 29 - 7
src/authProvider.ts

@@ -6,6 +6,20 @@ export const TOKEN_KEY = "byop-auth";
 export const REFRESH_TOKEN_KEY = "byop-refresh";
 export const USER_KEY = "byop-user";
 
+// Set up axios interceptor to automatically add Bearer token
+axios.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem(TOKEN_KEY);
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  (error) => {
+    return Promise.reject(error);
+  }
+);
+
 export const authProvider: AuthProvider = {
   login: async ({ email, password }) => {
     if (!email || !password) {
@@ -37,10 +51,13 @@ export const authProvider: AuthProvider = {
         // Assuming the backend might send user details in a nested 'user' object or top-level
         const userPayload = {
           id: response.data.user_id,
-          role: response.data.user_role,
-          // Add other user fields if available in response, e.g., username, email
-          // username: response.data.username, 
-          // email: response.data.email,
+          role: response.data.user_role || 'user',
+          username: email.split('@')[0], // Use email prefix as username for now
+          email: email,
+          preferences: {
+            theme: "light",
+            notifications: true,
+          }
         };
         localStorage.setItem(USER_KEY, JSON.stringify(userPayload));
         
@@ -248,12 +265,17 @@ export const authProvider: AuthProvider = {
         try {
           // Try to refresh the token
           const response = await axios.post(`${API_URL}/refresh-token`, {
-            refreshToken,
+            refresh_token: refreshToken,
           });
           
-          if (response.data && response.data.token) {
+          if (response.data && response.data.access_token) {
             // Update the token
-            localStorage.setItem(TOKEN_KEY, response.data.token);
+            localStorage.setItem(TOKEN_KEY, response.data.access_token);
+            
+            // Update refresh token if provided
+            if (response.data.refresh_token) {
+              localStorage.setItem(REFRESH_TOKEN_KEY, response.data.refresh_token);
+            }
             
             // Return to prevent redirecting to login
             return {

+ 131 - 0
src/pages/apps/create-choice.tsx

@@ -0,0 +1,131 @@
+import React from 'react';
+import { Card, Button, Row, Col, Typography, Space } from 'antd';
+import { 
+  PlusOutlined, 
+  FileTextOutlined, 
+  RocketOutlined,
+  CodeOutlined 
+} from '@ant-design/icons';
+import { useNavigate } from 'react-router';
+
+const { Title, Paragraph } = Typography;
+
+export const AppCreateChoice = () => {
+  const navigate = useNavigate();
+
+  return (
+    <div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
+      <Space direction="vertical" size="large" style={{ width: '100%' }}>
+        <div style={{ textAlign: 'center', marginBottom: '32px' }}>
+          <Title level={2}>
+            <RocketOutlined style={{ marginRight: '8px' }} />
+            Create New Application
+          </Title>
+          <Paragraph style={{ fontSize: '16px', color: '#666' }}>
+            Choose how you'd like to create your new application
+          </Paragraph>
+        </div>
+
+        <Row gutter={[24, 24]} justify="center">
+          <Col xs={24} md={12} lg={10}>
+            <Card
+              hoverable
+              style={{ height: '320px', cursor: 'pointer' }}
+              onClick={() => navigate('/apps/create/manual')}
+              cover={
+                <div style={{ 
+                  padding: '40px', 
+                  textAlign: 'center', 
+                  background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+                  color: 'white'
+                }}>
+                  <PlusOutlined style={{ fontSize: '48px' }} />
+                </div>
+              }
+            >
+              <Card.Meta
+                title={
+                  <Title level={4} style={{ marginBottom: '8px' }}>
+                    Create Manually
+                  </Title>
+                }
+                description={
+                  <Space direction="vertical" size="small">
+                    <Paragraph style={{ marginBottom: '16px' }}>
+                      Build your application step by step by selecting existing components 
+                      or creating new ones from scratch.
+                    </Paragraph>
+                    <ul style={{ paddingLeft: '20px', margin: 0 }}>
+                      <li>Full control over configuration</li>
+                      <li>Choose from existing components</li>
+                      <li>Customize every detail</li>
+                    </ul>
+                  </Space>
+                }
+              />
+            </Card>
+          </Col>
+
+          <Col xs={24} md={12} lg={10}>
+            <Card
+              hoverable
+              style={{ height: '320px', cursor: 'pointer' }}
+              onClick={() => navigate('/apps/import-compose')}
+              cover={
+                <div style={{ 
+                  padding: '40px', 
+                  textAlign: 'center', 
+                  background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
+                  color: 'white'
+                }}>
+                  <FileTextOutlined style={{ fontSize: '48px' }} />
+                </div>
+              }
+            >
+              <Card.Meta
+                title={
+                  <Title level={4} style={{ marginBottom: '8px' }}>
+                    Import from docker-compose.yml
+                  </Title>
+                }
+                description={
+                  <Space direction="vertical" size="small">
+                    <Paragraph style={{ marginBottom: '16px' }}>
+                      Quickly import an existing multi-service application by providing 
+                      your Git repository with a docker-compose.yml file.
+                    </Paragraph>
+                    <ul style={{ paddingLeft: '20px', margin: 0 }}>
+                      <li>Automatic service detection</li>
+                      <li>Fast onboarding</li>
+                      <li>Import existing projects</li>
+                    </ul>
+                  </Space>
+                }
+              />
+            </Card>
+          </Col>
+        </Row>
+
+        <div style={{ textAlign: 'center', marginTop: '32px' }}>
+          <Space size="large">
+            <Button 
+              size="large"
+              icon={<CodeOutlined />}
+              onClick={() => navigate('/apps/create/manual')}
+            >
+              Create Manually
+            </Button>
+            <Button 
+              type="primary" 
+              size="large"
+              icon={<FileTextOutlined />}
+              onClick={() => navigate('/apps/import-compose')}
+            >
+              Import from Compose
+            </Button>
+          </Space>
+        </div>
+      </Space>
+    </div>
+  );
+};

+ 55 - 0
src/pages/apps/create-manual.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import { Create, useForm, useSelect } from "@refinedev/antd";
+import { Form, Input, Select, Card } from 'antd';
+import { AppFormData } from '../../types/app';
+
+export const AppCreateManual = () => {
+    const { formProps, saveButtonProps } = useForm<AppFormData>();
+
+    const { selectProps: componentSelectProps } = useSelect({
+        resource: "components",
+        optionLabel: "name",
+        optionValue: "id",
+    });
+
+    return (
+        <Create saveButtonProps={saveButtonProps}>
+            <Form {...formProps} layout="vertical">
+                <Card title="App Information">
+                    <Form.Item
+                        label="Name"
+                        name="name"
+                        rules={[{ required: true, message: 'Please enter an app name' }]}
+                    >
+                        <Input placeholder="Enter app name" />
+                    </Form.Item>
+
+                    <Form.Item
+                        label="Version"
+                        name="version"
+                        rules={[{ required: true, message: 'Please enter the version' }]}
+                        initialValue="1.0.0"
+                    >
+                        <Input placeholder="E.g., 1.0.0" />
+                    </Form.Item>
+
+                    <Form.Item
+                        label="Components"
+                        name="components"
+                        rules={[{ required: true, message: 'Please select at least one component' }]}
+                    >
+                        <Select
+                            {...componentSelectProps}
+                            mode="multiple"
+                            placeholder="Select components to include in this app"
+                            showSearch
+                            filterOption={(input, option) =>
+                                String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                            }
+                        />
+                    </Form.Item>
+                </Card>
+            </Form>
+        </Create>
+    );
+};

+ 2 - 54
src/pages/apps/create.tsx

@@ -1,55 +1,3 @@
-import React from 'react';
-import { Create, useForm, useSelect } from "@refinedev/antd";
-import { Form, Input, Select, Card } from 'antd';
-import { AppFormData } from '../../types/app';
+import { AppCreateChoice } from './create-choice';
 
-export const AppCreate = () => {
-    const { formProps, saveButtonProps } = useForm<AppFormData>();
-
-    const { selectProps: componentSelectProps } = useSelect({
-        resource: "components",
-        optionLabel: "name",
-        optionValue: "id",
-    });
-
-    return (
-        <Create saveButtonProps={saveButtonProps}>
-            <Form {...formProps} layout="vertical">
-                <Card title="App Information">
-                    <Form.Item
-                        label="Name"
-                        name="name"
-                        rules={[{ required: true, message: 'Please enter an app name' }]}
-                    >
-                        <Input placeholder="Enter app name" />
-                    </Form.Item>
-
-                    <Form.Item
-                        label="Version"
-                        name="version"
-                        rules={[{ required: true, message: 'Please enter the version' }]}
-                        initialValue="1.0.0"
-                    >
-                        <Input placeholder="E.g., 1.0.0" />
-                    </Form.Item>
-
-                    <Form.Item
-                        label="Components"
-                        name="components"
-                        rules={[{ required: true, message: 'Please select at least one component' }]}
-                    >
-                        <Select
-                            {...componentSelectProps}
-                            mode="multiple"
-                            placeholder="Select components to include in this app"
-                            showSearch
-                            filterOption={(input, option) =>
-                                String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
-                            }
-                        />
-                    </Form.Item>
-                </Card>
-            </Form>
-        </Create>
-    );
-};
+export const AppCreate = AppCreateChoice;

+ 337 - 0
src/pages/apps/import-compose.tsx

@@ -0,0 +1,337 @@
+import React, { useState } from 'react';
+import { Create } from "@refinedev/antd";
+import { useCreate, useNotification } from "@refinedev/core";
+import { 
+  Form, 
+  Input, 
+  Button, 
+  Card, 
+  Steps, 
+  Alert, 
+  Typography, 
+  Space,
+  Divider,
+  Tag,
+  Spin
+} from 'antd';
+import { 
+  GitlabOutlined, 
+  FileTextOutlined, 
+  CheckCircleOutlined,
+  ArrowLeftOutlined 
+} from '@ant-design/icons';
+import { useNavigate } from 'react-router';
+import { API_URL } from '../../config';
+
+const { Title, Text, Paragraph } = Typography;
+const { Step } = Steps;
+
+interface ComposeImportForm {
+  app_name?: string;
+  source_url: string;
+  branch: string;
+  compose_path: string;
+}
+
+interface ParsedService {
+  name: string;
+  source: 'build' | 'image';
+  build_context?: string;
+  dockerfile?: string;
+  image?: string;
+  ports?: string[];
+  environment?: Record<string, string>;
+  volumes?: string[];
+}
+
+interface ComposeReviewResult {
+  app_name: string;
+  valid: boolean;
+  error?: string;
+  services: ParsedService[];
+}
+
+export const AppImportCompose = () => {
+  const [currentStep, setCurrentStep] = useState(0);
+  const [form] = Form.useForm();
+  const [parseResult, setParseResult] = useState<ComposeReviewResult | null>(null);
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+  const { open } = useNotification();
+  const { mutate: createApp } = useCreate();
+
+  const handleParseCompose = async (values: ComposeImportForm) => {
+    setLoading(true);
+    try {
+      // Call the backend API to parse the docker-compose.yml
+      const response = await fetch(`${API_URL}/apps/import/review`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${localStorage.getItem('byop-auth')}`,
+        },
+        body: JSON.stringify({
+          source_url: values.source_url,
+          branch: values.branch,
+          compose_path: values.compose_path,
+          app_name: values.app_name,
+        }),
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to parse docker-compose.yml');
+      }
+
+      const result: ComposeReviewResult = await response.json();
+      
+      if (!result.valid) {
+        open?.({
+          type: 'error',
+          message: 'Invalid Compose File',
+          description: result.error || 'The docker-compose.yml file could not be parsed.',
+        });
+        return;
+      }
+      
+      setParseResult(result);
+      setCurrentStep(1);
+    } catch (error) {
+      open?.({
+        type: 'error',
+        message: 'Parse Error',
+        description: 'Failed to parse the docker-compose.yml file. Please check your repository URL and file path.',
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleCreateApp = async () => {
+    if (!parseResult) return;
+
+    setLoading(true);
+    try {
+      // Call the backend API to create the app and components
+      const response = await fetch(`${API_URL}/apps/import/create`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${localStorage.getItem('byop-auth')}`,
+        },
+        body: JSON.stringify({
+          source_url: form.getFieldValue('source_url'),
+          branch: form.getFieldValue('branch'),
+          compose_path: form.getFieldValue('compose_path'),
+          app_name: form.getFieldValue('app_name'),
+          confirmed_app_name: parseResult.app_name,
+        }),
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to create app from compose');
+      }
+
+      const createdApp = await response.json();
+      
+      open?.({
+        type: 'success',
+        message: 'App Created Successfully',
+        description: `Your app "${parseResult.app_name}" has been created with ${parseResult.services.length} components.`,
+      });
+
+      // Redirect to the new app's show page
+      navigate(`/apps/show/${createdApp.id}`);
+    } catch (error) {
+      open?.({
+        type: 'error',
+        message: 'Creation Error',
+        description: 'Failed to create the app. Please try again.',
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const renderImportForm = () => (
+    <Card title="Import from docker-compose.yml" extra={
+      <Button 
+        type="text" 
+        icon={<ArrowLeftOutlined />} 
+        onClick={() => navigate('/apps/create')}
+      >
+        Back to Create Options
+      </Button>
+    }>
+      <Alert
+        message="Import your existing multi-service application"
+        description="Provide your Git repository details and we'll parse your docker-compose.yml to automatically create components for each service."
+        type="info"
+        showIcon
+        style={{ marginBottom: 24 }}
+      />
+
+      <Form 
+        form={form}
+        layout="vertical" 
+        onFinish={handleParseCompose}
+        initialValues={{
+          branch: 'main',
+          compose_path: 'docker-compose.yml'
+        }}
+      >
+        <Form.Item
+          label="App Name (Optional)"
+          name="app_name"
+          help="Leave empty to use the repository name"
+        >
+          <Input placeholder="e.g., my-awesome-app" />
+        </Form.Item>
+
+        <Form.Item
+          label="Git Repository URL"
+          name="source_url"
+          rules={[
+            { required: true, message: 'Please enter the Git repository URL' },
+            { type: 'url', message: 'Please enter a valid URL' }
+          ]}
+        >
+          <Input 
+            prefix={<GitlabOutlined />}
+            placeholder="https://github.com/username/repository.git" 
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="Branch"
+          name="branch"
+          rules={[{ required: true, message: 'Please enter the branch name' }]}
+        >
+          <Input placeholder="main" />
+        </Form.Item>
+
+        <Form.Item
+          label="Path to Compose File"
+          name="compose_path"
+          rules={[{ required: true, message: 'Please enter the path to docker-compose.yml' }]}
+        >
+          <Input 
+            prefix={<FileTextOutlined />}
+            placeholder="docker-compose.yml" 
+          />
+        </Form.Item>
+
+        <Form.Item>
+          <Button 
+            type="primary" 
+            htmlType="submit" 
+            loading={loading}
+            size="large"
+          >
+            Parse Compose File
+          </Button>
+        </Form.Item>
+      </Form>
+    </Card>
+  );
+
+  const renderReviewStep = () => (
+    <Card title="Review Your Application">
+      <Space direction="vertical" style={{ width: '100%' }} size="large">
+        <Alert
+          message={`We found ${parseResult?.services.length} services in your docker-compose.yml`}
+          description="We will create a component for each service. Review the details below and click 'Create App and Components' to proceed."
+          type="success"
+          showIcon
+        />
+
+        <div>
+          <Title level={4}>App Name: {parseResult?.app_name}</Title>
+          <Divider />
+          
+          <Title level={5}>Services Found:</Title>
+          <Space direction="vertical" style={{ width: '100%' }} size={16}>
+            {parseResult?.services.map((service: ParsedService, index: number) => (
+              <Card 
+                key={index} 
+                size="small" 
+                title={
+                  <Space>
+                    <Tag color="blue">{service.name}</Tag>
+                    <Text strong>Service: {service.name}</Text>
+                  </Space>
+                }
+              >
+                <Space direction="vertical" size="small">
+                  {service.source === 'build' && service.build_context && (
+                    <Text>
+                      <strong>Source:</strong> Will be built from the <code>{service.build_context}</code> directory.
+                      {service.dockerfile && <span> Using <code>{service.dockerfile}</code></span>}
+                    </Text>
+                  )}
+                  {service.source === 'image' && service.image && (
+                    <Text>
+                      <strong>Source:</strong> Uses the public image <code>{service.image}</code>.
+                    </Text>
+                  )}
+                  {service.ports && service.ports.length > 0 && (
+                    <Text>
+                      <strong>Ports:</strong> {service.ports.join(', ')}
+                    </Text>
+                  )}
+                  {service.environment && Object.keys(service.environment).length > 0 && (
+                    <Text>
+                      <strong>Environment Variables:</strong> {Object.keys(service.environment).length} variables defined
+                    </Text>
+                  )}
+                  {service.volumes && service.volumes.length > 0 && (
+                    <Text>
+                      <strong>Volumes:</strong> {service.volumes.length} volume mappings
+                    </Text>
+                  )}
+                </Space>
+              </Card>
+            ))}
+          </Space>
+        </div>
+
+        <Space>
+          <Button 
+            onClick={() => setCurrentStep(0)}
+            disabled={loading}
+          >
+            Back to Edit
+          </Button>
+          <Button 
+            type="primary" 
+            size="large"
+            loading={loading}
+            onClick={handleCreateApp}
+            icon={<CheckCircleOutlined />}
+          >
+            Create App and Components
+          </Button>
+        </Space>
+      </Space>
+    </Card>
+  );
+
+  return (
+    <Create 
+      title="Import from docker-compose.yml"
+      breadcrumb={false}
+      headerButtons={[]}
+    >
+      <Space direction="vertical" style={{ width: '100%' }} size="large">
+        <Steps current={currentStep} size="small">
+          <Step title="Import Details" description="Repository and compose file info" />
+          <Step title="Review & Confirm" description="Verify services and create app" />
+        </Steps>
+
+        <Spin spinning={loading}>
+          {currentStep === 0 && renderImportForm()}
+          {currentStep === 1 && renderReviewStep()}
+        </Spin>
+      </Space>
+    </Create>
+  );
+};

+ 3 - 0
src/pages/apps/index.ts

@@ -1,4 +1,7 @@
 // Renamed from blueprints/index.ts
 export * from "./list";
 export * from "./create";
+export * from "./create-choice";
+export * from "./create-manual";
+export * from "./import-compose";
 export * from "./show";