123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- 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>
- );
- };
|