|
@@ -0,0 +1,902 @@
|
|
|
+import React, { useState } from 'react';
|
|
|
+import { Steps, Card, Button, Form, Input, Select, Typography, Row, Col, Result, Space, Alert, Modal, message } from 'antd';
|
|
|
+import { useCreate, useDelete, useOne } from '@refinedev/core';
|
|
|
+import {
|
|
|
+ RocketOutlined,
|
|
|
+ CheckCircleOutlined,
|
|
|
+ PlayCircleOutlined,
|
|
|
+ AppstoreOutlined,
|
|
|
+ SettingOutlined,
|
|
|
+ UserOutlined,
|
|
|
+ ThunderboltOutlined,
|
|
|
+ PlusOutlined,
|
|
|
+ DeleteOutlined
|
|
|
+} from '@ant-design/icons';
|
|
|
+import { ComponentStatusDisplay, ComponentErrorDisplay, PreviewControl } from '../../components';
|
|
|
+import { delayedComponentFetch } from '../../utils/componentRetry';
|
|
|
+
|
|
|
+const { Title, Text, Paragraph } = Typography;
|
|
|
+const { Step } = Steps;
|
|
|
+
|
|
|
+interface ComponentData {
|
|
|
+ name: string;
|
|
|
+ version: string;
|
|
|
+ type: string;
|
|
|
+ repository: string;
|
|
|
+ branch: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface WizardData {
|
|
|
+ client: {
|
|
|
+ name: string;
|
|
|
+ contactEmail: string;
|
|
|
+ organization: string;
|
|
|
+ };
|
|
|
+ components: ComponentData[];
|
|
|
+ app: {
|
|
|
+ name: string;
|
|
|
+ version: string;
|
|
|
+ components: (string | number)[];
|
|
|
+ };
|
|
|
+ deployment: {
|
|
|
+ appId: string | number;
|
|
|
+ environment: string;
|
|
|
+ version: string;
|
|
|
+ clientId: string | number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export const SetupWizard = () => {
|
|
|
+ const [currentStep, setCurrentStep] = useState(0);
|
|
|
+ const [wizardData, setWizardData] = useState<Partial<WizardData>>({
|
|
|
+ components: []
|
|
|
+ });
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [createdIds, setCreatedIds] = useState<{
|
|
|
+ clientId?: string | number;
|
|
|
+ componentIds: (string | number)[];
|
|
|
+ appId?: string | number;
|
|
|
+ deploymentId?: string | number;
|
|
|
+ }>({
|
|
|
+ componentIds: []
|
|
|
+ });
|
|
|
+
|
|
|
+ // Form instances for each step
|
|
|
+ const [clientForm] = Form.useForm();
|
|
|
+ const [componentForm] = Form.useForm();
|
|
|
+ const [appForm] = Form.useForm();
|
|
|
+ const [deploymentForm] = Form.useForm();
|
|
|
+
|
|
|
+ const { mutate: createClient } = useCreate();
|
|
|
+ const { mutate: createComponent } = useCreate();
|
|
|
+ const { mutate: createApp } = useCreate();
|
|
|
+ const { mutate: createDeployment } = useCreate();
|
|
|
+ const { mutate: deleteComponent } = useDelete();
|
|
|
+
|
|
|
+ const steps = [
|
|
|
+ {
|
|
|
+ title: 'Welcome',
|
|
|
+ icon: <PlayCircleOutlined />,
|
|
|
+ description: 'Get started with BYOP'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Create Client',
|
|
|
+ icon: <UserOutlined />,
|
|
|
+ description: 'Set up your organization'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Components',
|
|
|
+ icon: <AppstoreOutlined />,
|
|
|
+ description: 'Define your components'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Create App',
|
|
|
+ icon: <SettingOutlined />,
|
|
|
+ description: 'Bundle components into an app'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Deploy',
|
|
|
+ icon: <RocketOutlined />,
|
|
|
+ description: 'Create your first deployment'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Complete',
|
|
|
+ icon: <CheckCircleOutlined />,
|
|
|
+ description: 'You\'re all set!'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const handleNext = () => {
|
|
|
+ setCurrentStep(currentStep + 1);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePrev = () => {
|
|
|
+ setCurrentStep(currentStep - 1);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAddComponent = async (values: ComponentData) => {
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ createComponent({
|
|
|
+ resource: 'components',
|
|
|
+ values: values
|
|
|
+ }, {
|
|
|
+ onSuccess: async (data) => {
|
|
|
+ const componentId = typeof data.data.id === 'string' ? parseInt(data.data.id) : data.data.id;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Wait for component status to be available with retry mechanism
|
|
|
+ await delayedComponentFetch(
|
|
|
+ async () => {
|
|
|
+ const response = await fetch(`http://localhost:8000/api/v1/components/${componentId}`, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const componentData = await response.json();
|
|
|
+ if (!componentData.status) {
|
|
|
+ throw new Error('Component status not available yet');
|
|
|
+ }
|
|
|
+
|
|
|
+ return componentData;
|
|
|
+ },
|
|
|
+ { maxRetries: 5, delayMs: 1000, backoffMultiplier: 1.2 }
|
|
|
+ );
|
|
|
+
|
|
|
+ setCreatedIds(prev => ({
|
|
|
+ ...prev,
|
|
|
+ componentIds: [...prev.componentIds, componentId].filter((id): id is string | number => id !== undefined)
|
|
|
+ }));
|
|
|
+ setWizardData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ components: [...(prev.components || []), values]
|
|
|
+ }));
|
|
|
+ // Reset form for next component
|
|
|
+ componentForm.resetFields();
|
|
|
+ message.success(`Component "${values.name}" created successfully!`);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Component created but status polling failed:', error);
|
|
|
+ // Still add the component since creation succeeded
|
|
|
+ setCreatedIds(prev => ({
|
|
|
+ ...prev,
|
|
|
+ componentIds: [...prev.componentIds, componentId].filter((id): id is string | number => id !== undefined)
|
|
|
+ }));
|
|
|
+ setWizardData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ components: [...(prev.components || []), values]
|
|
|
+ }));
|
|
|
+ componentForm.resetFields();
|
|
|
+ message.warning(`Component "${values.name}" created, but status may take a moment to update.`);
|
|
|
+ }
|
|
|
+
|
|
|
+ setLoading(false);
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Error creating component:', error);
|
|
|
+ message.error('Failed to create component. Please try again.');
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDeleteComponent = (componentId: string | number, index: number) => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: 'Delete Component',
|
|
|
+ content: `Are you sure you want to delete "${wizardData.components?.[index]?.name || 'this component'}"? This action cannot be undone.`,
|
|
|
+ okText: 'Yes, Delete',
|
|
|
+ okType: 'danger',
|
|
|
+ cancelText: 'Cancel',
|
|
|
+ onOk: () => {
|
|
|
+ deleteComponent({
|
|
|
+ resource: 'components',
|
|
|
+ id: componentId
|
|
|
+ }, {
|
|
|
+ onSuccess: () => {
|
|
|
+ // Remove from local state
|
|
|
+ setCreatedIds(prev => ({
|
|
|
+ ...prev,
|
|
|
+ componentIds: prev.componentIds.filter(id => id !== componentId)
|
|
|
+ }));
|
|
|
+ setWizardData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ components: prev.components?.filter((_, i) => i !== index) || []
|
|
|
+ }));
|
|
|
+ message.success('Component deleted successfully');
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Error deleting component:', error);
|
|
|
+ message.error('Failed to delete component');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleStepSubmit = async (values: any) => {
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ switch (currentStep) {
|
|
|
+ case 1: // Create Client
|
|
|
+ createClient({
|
|
|
+ resource: 'clients',
|
|
|
+ values: values
|
|
|
+ }, {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setCreatedIds(prev => ({ ...prev, clientId: data.data.id }));
|
|
|
+ setWizardData(prev => ({ ...prev, client: values }));
|
|
|
+ handleNext();
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Error creating client:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 3: // Create App
|
|
|
+ const appData = {
|
|
|
+ ...values,
|
|
|
+ components: createdIds.componentIds
|
|
|
+ };
|
|
|
+ createApp({
|
|
|
+ resource: 'apps',
|
|
|
+ values: appData
|
|
|
+ }, {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setCreatedIds(prev => ({ ...prev, appId: data.data.id }));
|
|
|
+ setWizardData(prev => ({ ...prev, app: appData }));
|
|
|
+ // Don't automatically proceed to next step - let user interact with preview
|
|
|
+ setLoading(false);
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Error creating app:', error);
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 4: // Create Deployment
|
|
|
+ const deploymentData = {
|
|
|
+ ...values,
|
|
|
+ appId: createdIds.appId,
|
|
|
+ clientId: createdIds.clientId
|
|
|
+ };
|
|
|
+ createDeployment({
|
|
|
+ resource: 'deployments',
|
|
|
+ values: deploymentData
|
|
|
+ }, {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setCreatedIds(prev => ({ ...prev, deploymentId: data.data.id }));
|
|
|
+ setWizardData(prev => ({ ...prev, deployment: deploymentData }));
|
|
|
+ handleNext();
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Error creating deployment:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Step submission error:', error);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const fillSampleData = () => {
|
|
|
+ const sampleData = {
|
|
|
+ name: "web-frontend",
|
|
|
+ version: "1.0.0",
|
|
|
+ type: "frontend",
|
|
|
+ repository: "https://github.com/acme/web-frontend",
|
|
|
+ branch: "main"
|
|
|
+ };
|
|
|
+ componentForm.setFieldsValue(sampleData);
|
|
|
+ };
|
|
|
+
|
|
|
+ const fillSampleClientData = () => {
|
|
|
+ const sampleClientData = {
|
|
|
+ name: "Acme Corporation",
|
|
|
+ contactEmail: "admin@acme.com",
|
|
|
+ organization: "Acme Corp"
|
|
|
+ };
|
|
|
+ clientForm.setFieldsValue(sampleClientData);
|
|
|
+ message.success('Sample client data filled!');
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderStepContent = () => {
|
|
|
+ switch (currentStep) {
|
|
|
+ case 0: // Welcome
|
|
|
+ return (
|
|
|
+ <Card style={{ textAlign: 'center', border: 'none', maxWidth: '800px', margin: '0 auto' }}>
|
|
|
+ <RocketOutlined style={{ fontSize: '64px', color: '#1890ff', marginBottom: '24px' }} />
|
|
|
+ <Title level={2}>Welcome to BYOP Dashboard!</Title>
|
|
|
+ <Paragraph style={{ fontSize: '16px', marginBottom: '32px', maxWidth: '600px', margin: '0 auto 32px' }}>
|
|
|
+ This setup wizard will help you create your first application with multiple components and preview functionality.
|
|
|
+ </Paragraph>
|
|
|
+ <div style={{ marginTop: '40px' }}>
|
|
|
+ <Button type="primary" size="large" onClick={handleNext}>
|
|
|
+ Get Started
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+
|
|
|
+ case 1: // Create Client
|
|
|
+ return (
|
|
|
+ <Card
|
|
|
+ title="Create Your Client Profile"
|
|
|
+ style={{
|
|
|
+ maxWidth: 'min(600px, 100%)',
|
|
|
+ margin: '0 auto',
|
|
|
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
|
|
+ }}
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="dashed"
|
|
|
+ onClick={fillSampleClientData}
|
|
|
+ icon={<ThunderboltOutlined />}
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ Fill Sample Data
|
|
|
+ </Button>
|
|
|
+ }>
|
|
|
+ <Form
|
|
|
+ form={clientForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleStepSubmit}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ label="Organization Name"
|
|
|
+ name="name"
|
|
|
+ rules={[{ required: true, message: 'Please enter your organization name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="e.g., Acme Corporation" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Contact Email"
|
|
|
+ name="contactEmail"
|
|
|
+ rules={[
|
|
|
+ { required: true, message: 'Please enter contact email' },
|
|
|
+ { type: 'email', message: 'Please enter a valid email' }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input placeholder="admin@acme.com" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Organization"
|
|
|
+ name="organization"
|
|
|
+ rules={[{ required: true, message: 'Please enter organization name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="Acme Corp" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item style={{ marginTop: '32px', textAlign: 'right' }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handlePrev} size="large">Previous</Button>
|
|
|
+ <Button type="primary" htmlType="submit" loading={loading} size="large">
|
|
|
+ Create Client & Continue
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+
|
|
|
+ case 2: // Create Components
|
|
|
+ return (
|
|
|
+ <Card
|
|
|
+ title="Create Your Components"
|
|
|
+ style={{
|
|
|
+ maxWidth: 'min(800px, 100%)',
|
|
|
+ margin: '0 auto',
|
|
|
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
|
|
+ }}
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="dashed"
|
|
|
+ onClick={fillSampleData}
|
|
|
+ icon={<ThunderboltOutlined />}
|
|
|
+ >
|
|
|
+ Fill Sample Data
|
|
|
+ </Button>
|
|
|
+ }>
|
|
|
+
|
|
|
+ <Paragraph style={{ marginBottom: '24px', color: '#666' }}>
|
|
|
+ Components are the building blocks of your applications. Create one or more components for your app.
|
|
|
+ </Paragraph>
|
|
|
+
|
|
|
+ <Alert
|
|
|
+ message="Component Validation & Building"
|
|
|
+ description="After creation, your components will be automatically validated and built. You can monitor their status below."
|
|
|
+ type="info"
|
|
|
+ showIcon
|
|
|
+ style={{ marginBottom: '24px' }}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* Display existing components */}
|
|
|
+ {createdIds.componentIds.length > 0 && (
|
|
|
+ <div style={{ marginBottom: '24px' }}>
|
|
|
+ <Title level={5}>Created Components ({createdIds.componentIds.length}):</Title>
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ {createdIds.componentIds.map((componentId, index) => (
|
|
|
+ <Col key={componentId} xs={24} sm={12}>
|
|
|
+ <Card
|
|
|
+ size="small"
|
|
|
+ style={{ backgroundColor: '#f6ffed' }}
|
|
|
+ extra={
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ danger
|
|
|
+ size="small"
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
+ onClick={() => handleDeleteComponent(componentId, index)}
|
|
|
+ title="Delete component"
|
|
|
+ />
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
+ <div>
|
|
|
+ <Text strong>{wizardData.components?.[index]?.name || `Component ${index + 1}`}:</Text>
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">{wizardData.components?.[index]?.type} • {wizardData.components?.[index]?.version}</Text>
|
|
|
+ </div>
|
|
|
+ <ComponentStatusDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <ComponentErrorDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ ))}
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Component form */}
|
|
|
+ <Card
|
|
|
+ title="Add New Component"
|
|
|
+ size="small"
|
|
|
+ style={{ marginBottom: '24px' }}
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={componentForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleAddComponent}
|
|
|
+ >
|
|
|
+ <Row gutter={16}>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Form.Item
|
|
|
+ label="Component Name"
|
|
|
+ name="name"
|
|
|
+ rules={[{ required: true, message: 'Please enter component name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="e.g., web-frontend" />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Form.Item
|
|
|
+ label="Version"
|
|
|
+ name="version"
|
|
|
+ rules={[{ required: true, message: 'Please enter version' }]}
|
|
|
+ initialValue="1.0.0"
|
|
|
+ >
|
|
|
+ <Input placeholder="1.0.0" />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <Row gutter={16}>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Form.Item
|
|
|
+ label="Component Type"
|
|
|
+ name="type"
|
|
|
+ rules={[{ required: true, message: 'Please select component type' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="Select component type">
|
|
|
+ <Select.Option value="frontend">Frontend</Select.Option>
|
|
|
+ <Select.Option value="backend">Backend</Select.Option>
|
|
|
+ <Select.Option value="api">API</Select.Option>
|
|
|
+ <Select.Option value="database">Database</Select.Option>
|
|
|
+ <Select.Option value="microservice">Microservice</Select.Option>
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Form.Item
|
|
|
+ label="Branch"
|
|
|
+ name="branch"
|
|
|
+ rules={[{ required: true, message: 'Please enter branch name' }]}
|
|
|
+ initialValue="main"
|
|
|
+ >
|
|
|
+ <Input placeholder="main" />
|
|
|
+ </Form.Item>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Git Repository URL"
|
|
|
+ name="repository"
|
|
|
+ rules={[
|
|
|
+ { required: true, message: 'Please enter Git URL' },
|
|
|
+ { type: 'url', message: 'Please enter a valid URL' }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Input placeholder="https://github.com/username/repo" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item style={{ textAlign: 'right' }}>
|
|
|
+ <Button
|
|
|
+ type="default"
|
|
|
+ htmlType="submit"
|
|
|
+ loading={loading}
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ >
|
|
|
+ Add Component
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <div style={{ textAlign: 'right' }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handlePrev} size="large">Previous</Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ onClick={handleNext}
|
|
|
+ size="large"
|
|
|
+ disabled={createdIds.componentIds.length === 0}
|
|
|
+ >
|
|
|
+ Continue to App Creation ({createdIds.componentIds.length} component{createdIds.componentIds.length !== 1 ? 's' : ''})
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+
|
|
|
+ case 3: // Create App
|
|
|
+ return (
|
|
|
+ <Card
|
|
|
+ title="Create Your Application"
|
|
|
+ style={{
|
|
|
+ maxWidth: 'min(800px, 100%)',
|
|
|
+ margin: '0 auto',
|
|
|
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
|
|
+ }}>
|
|
|
+ <Paragraph style={{ marginBottom: '24px', color: '#666' }}>
|
|
|
+ Now let's bundle your components into an application that can be deployed and previewed.
|
|
|
+ </Paragraph>
|
|
|
+
|
|
|
+ {!createdIds.appId ? (
|
|
|
+ // App Creation Form
|
|
|
+ <Form
|
|
|
+ form={appForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleStepSubmit}
|
|
|
+ >
|
|
|
+ {/* Component Status Overview */}
|
|
|
+ <div style={{ marginBottom: '24px' }}>
|
|
|
+ <Title level={5}>Components in this App ({createdIds.componentIds.length}):</Title>
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ {createdIds.componentIds.map((componentId, index) => (
|
|
|
+ <Col key={componentId} xs={24} sm={12} md={8}>
|
|
|
+ <Card size="small" style={{ backgroundColor: '#f6ffed' }}>
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
|
+ <div>
|
|
|
+ <Text strong>{wizardData.components?.[index]?.name}:</Text>
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">{wizardData.components?.[index]?.type}</Text>
|
|
|
+ </div>
|
|
|
+ <ComponentStatusDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <ComponentErrorDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ ))}
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Application Name"
|
|
|
+ name="name"
|
|
|
+ rules={[{ required: true, message: 'Please enter application name' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="e.g., acme-web-app" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Application Version"
|
|
|
+ name="version"
|
|
|
+ rules={[{ required: true, message: 'Please enter version' }]}
|
|
|
+ initialValue="1.0.0"
|
|
|
+ >
|
|
|
+ <Input placeholder="1.0.0" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item style={{ marginTop: '32px', textAlign: 'right' }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handlePrev} size="large">Previous</Button>
|
|
|
+ <Button type="primary" htmlType="submit" loading={loading} size="large">
|
|
|
+ Create App
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ ) : (
|
|
|
+ // App Created - Show Preview Options
|
|
|
+ <div>
|
|
|
+ <Alert
|
|
|
+ message="Application Created Successfully!"
|
|
|
+ description={`Your app "${wizardData.app?.name}" has been created with ${createdIds.componentIds.length} component${createdIds.componentIds.length !== 1 ? 's' : ''}. You can now create a preview to test your application before deployment.`}
|
|
|
+ type="success"
|
|
|
+ showIcon
|
|
|
+ style={{ marginBottom: '24px' }}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* Component Status Overview */}
|
|
|
+ <div style={{ marginBottom: '24px' }}>
|
|
|
+ <Title level={5}>Components in this App ({createdIds.componentIds.length}):</Title>
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ {createdIds.componentIds.map((componentId, index) => (
|
|
|
+ <Col key={componentId} xs={24} sm={12} md={8}>
|
|
|
+ <Card size="small" style={{ backgroundColor: '#f6ffed' }}>
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
|
+ <div>
|
|
|
+ <Text strong>{wizardData.components?.[index]?.name}:</Text>
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">{wizardData.components?.[index]?.type}</Text>
|
|
|
+ </div>
|
|
|
+ <ComponentStatusDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <ComponentErrorDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ ))}
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* App Preview Control */}
|
|
|
+ <div style={{ marginBottom: '24px' }}>
|
|
|
+ <Title level={5}>Application Preview:</Title>
|
|
|
+ <PreviewControl
|
|
|
+ appId={typeof createdIds.appId === 'string' ? parseInt(createdIds.appId) : createdIds.appId}
|
|
|
+ appName={wizardData.app?.name || 'Application'}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Alert
|
|
|
+ message="Ready to Deploy"
|
|
|
+ description="Your application is ready! You can create previews to test your app, or continue to the deployment step to make it live in a specific environment."
|
|
|
+ type="info"
|
|
|
+ showIcon
|
|
|
+ style={{ marginBottom: '24px' }}
|
|
|
+ />
|
|
|
+
|
|
|
+ <div style={{ textAlign: 'right' }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handlePrev} size="large">Previous</Button>
|
|
|
+ <Button type="primary" onClick={handleNext} size="large">
|
|
|
+ Continue to Deployment
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+
|
|
|
+ case 4: // Create Deployment
|
|
|
+ return (
|
|
|
+ <Card
|
|
|
+ title="Deploy Your Application"
|
|
|
+ style={{
|
|
|
+ maxWidth: 'min(700px, 100%)',
|
|
|
+ margin: '0 auto',
|
|
|
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
|
|
+ }}>
|
|
|
+ <Form
|
|
|
+ form={deploymentForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleStepSubmit}
|
|
|
+ >
|
|
|
+ <Paragraph style={{ marginBottom: '24px', color: '#666' }}>
|
|
|
+ Great! Your application is created and can be previewed. Now let's deploy it to a specific environment for permanent access.
|
|
|
+ </Paragraph>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Environment"
|
|
|
+ name="environment"
|
|
|
+ rules={[{ required: true, message: 'Please select environment' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="Select deployment environment" size="large">
|
|
|
+ <Select.Option value="dev">Development</Select.Option>
|
|
|
+ <Select.Option value="preprod">Pre-Production</Select.Option>
|
|
|
+ <Select.Option value="prod">Production</Select.Option>
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="Deployment Version"
|
|
|
+ name="version"
|
|
|
+ rules={[{ required: true, message: 'Please enter version' }]}
|
|
|
+ initialValue="1.0.0"
|
|
|
+ >
|
|
|
+ <Input placeholder="1.0.0" size="large" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item style={{ textAlign: 'right', marginTop: '24px' }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handlePrev} size="large">Previous</Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ htmlType="submit"
|
|
|
+ loading={loading}
|
|
|
+ size="large"
|
|
|
+ icon={<RocketOutlined />}
|
|
|
+ >
|
|
|
+ Deploy Application
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Card>
|
|
|
+ );
|
|
|
+
|
|
|
+ case 5: // Complete
|
|
|
+ return (
|
|
|
+ <Result
|
|
|
+ status="success"
|
|
|
+ title="Congratulations! Your application is ready!"
|
|
|
+ subTitle={`You've successfully created ${createdIds.componentIds.length} component${createdIds.componentIds.length !== 1 ? 's' : ''}, bundled them into an app, and deployed it.`}
|
|
|
+ extra={[
|
|
|
+ <Button type="primary" size="large" key="dashboard" href="/dashboard">
|
|
|
+ Go to Dashboard
|
|
|
+ </Button>,
|
|
|
+ <Button size="large" key="apps" href="/apps">
|
|
|
+ View Apps
|
|
|
+ </Button>,
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <div style={{ textAlign: 'left', marginTop: '24px' }}>
|
|
|
+ <Row gutter={[24, 24]}>
|
|
|
+ {/* Summary */}
|
|
|
+ <Col span={24}>
|
|
|
+ <Card>
|
|
|
+ <Title level={4}>What you've created:</Title>
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Card size="small">
|
|
|
+ <UserOutlined style={{ color: '#52c41a', marginRight: '8px' }} />
|
|
|
+ <Text strong>Client:</Text> {wizardData.client?.name}
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">ID: {createdIds.clientId}</Text>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Card size="small">
|
|
|
+ <AppstoreOutlined style={{ color: '#1890ff', marginRight: '8px' }} />
|
|
|
+ <Text strong>Components:</Text> {createdIds.componentIds.length} created
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">All validated and ready</Text>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Card size="small">
|
|
|
+ <SettingOutlined style={{ color: '#722ed1', marginRight: '8px' }} />
|
|
|
+ <Text strong>App:</Text> {wizardData.app?.name}
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">ID: {createdIds.appId}</Text>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12}>
|
|
|
+ <Card size="small">
|
|
|
+ <RocketOutlined style={{ color: '#fa541c', marginRight: '8px' }} />
|
|
|
+ <Text strong>Deployment:</Text> {wizardData.deployment?.environment?.toUpperCase()}
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">ID: {createdIds.deploymentId}</Text>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+
|
|
|
+ {/* Component Status */}
|
|
|
+ <Col span={24}>
|
|
|
+ <Card>
|
|
|
+ <Title level={4}>Component Status:</Title>
|
|
|
+ <Row gutter={[16, 16]}>
|
|
|
+ {createdIds.componentIds.map((componentId, index) => (
|
|
|
+ <Col key={componentId} xs={24} sm={12} md={8}>
|
|
|
+ <Card size="small">
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
|
+ <div>
|
|
|
+ <Text strong>{wizardData.components?.[index]?.name}:</Text>
|
|
|
+ <br />
|
|
|
+ <Text type="secondary">{wizardData.components?.[index]?.type}</Text>
|
|
|
+ </div>
|
|
|
+ <ComponentStatusDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <ComponentErrorDisplay
|
|
|
+ componentId={typeof componentId === 'string' ? parseInt(componentId) : componentId}
|
|
|
+ />
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ ))}
|
|
|
+ </Row>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+
|
|
|
+ {/* App Preview Section */}
|
|
|
+ {createdIds.appId && wizardData.app?.name && (
|
|
|
+ <Col span={24}>
|
|
|
+ <PreviewControl
|
|
|
+ appId={typeof createdIds.appId === 'string' ? parseInt(createdIds.appId) : createdIds.appId}
|
|
|
+ appName={wizardData.app.name}
|
|
|
+ />
|
|
|
+ </Col>
|
|
|
+ )}
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ </Result>
|
|
|
+ );
|
|
|
+
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{
|
|
|
+ padding: '8px 16px',
|
|
|
+ maxWidth: '1200px',
|
|
|
+ margin: '0 auto',
|
|
|
+ minHeight: 'calc(100vh - 120px)',
|
|
|
+ background: '#fafafa'
|
|
|
+ }}>
|
|
|
+ <Card style={{ marginBottom: '24px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
|
|
+ <Steps current={currentStep} responsive={true} size="small">
|
|
|
+ {steps.map((step, index) => (
|
|
|
+ <Step
|
|
|
+ key={index}
|
|
|
+ title={step.title}
|
|
|
+ description={step.description}
|
|
|
+ icon={step.icon}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </Steps>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <div style={{ minHeight: '500px' }}>
|
|
|
+ {renderStepContent()}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default SetupWizard;
|