|
@@ -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>
|
|
|
+ );
|
|
|
+};
|