import-compose.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import React, { useState } from 'react';
  2. import { Create } from "@refinedev/antd";
  3. import { useCreate, useNotification } from "@refinedev/core";
  4. import {
  5. Form,
  6. Input,
  7. Button,
  8. Card,
  9. Steps,
  10. Alert,
  11. Typography,
  12. Space,
  13. Divider,
  14. Tag,
  15. Spin
  16. } from 'antd';
  17. import {
  18. GitlabOutlined,
  19. FileTextOutlined,
  20. CheckCircleOutlined,
  21. ArrowLeftOutlined
  22. } from '@ant-design/icons';
  23. import { useNavigate } from 'react-router';
  24. import { API_URL } from '../../config';
  25. const { Title, Text, Paragraph } = Typography;
  26. const { Step } = Steps;
  27. interface ComposeImportForm {
  28. app_name?: string;
  29. source_url: string;
  30. branch: string;
  31. compose_path: string;
  32. }
  33. interface ParsedService {
  34. name: string;
  35. source: 'build' | 'image';
  36. build_context?: string;
  37. dockerfile?: string;
  38. image?: string;
  39. ports?: string[];
  40. environment?: Record<string, string>;
  41. volumes?: string[];
  42. }
  43. interface ComposeReviewResult {
  44. app_name: string;
  45. valid: boolean;
  46. error?: string;
  47. services: ParsedService[];
  48. }
  49. export const AppImportCompose = () => {
  50. const [currentStep, setCurrentStep] = useState(0);
  51. const [form] = Form.useForm();
  52. const [parseResult, setParseResult] = useState<ComposeReviewResult | null>(null);
  53. const [loading, setLoading] = useState(false);
  54. const navigate = useNavigate();
  55. const { open } = useNotification();
  56. const { mutate: createApp } = useCreate();
  57. const handleParseCompose = async (values: ComposeImportForm) => {
  58. setLoading(true);
  59. try {
  60. // Call the backend API to parse the docker-compose.yml
  61. const response = await fetch(`${API_URL}/apps/import/review`, {
  62. method: 'POST',
  63. headers: {
  64. 'Content-Type': 'application/json',
  65. 'Authorization': `Bearer ${localStorage.getItem('byop-auth')}`,
  66. },
  67. body: JSON.stringify({
  68. source_url: values.source_url,
  69. branch: values.branch,
  70. compose_path: values.compose_path,
  71. app_name: values.app_name,
  72. }),
  73. });
  74. if (!response.ok) {
  75. throw new Error('Failed to parse docker-compose.yml');
  76. }
  77. const result: ComposeReviewResult = await response.json();
  78. if (!result.valid) {
  79. open?.({
  80. type: 'error',
  81. message: 'Invalid Compose File',
  82. description: result.error || 'The docker-compose.yml file could not be parsed.',
  83. });
  84. return;
  85. }
  86. setParseResult(result);
  87. setCurrentStep(1);
  88. } catch (error) {
  89. open?.({
  90. type: 'error',
  91. message: 'Parse Error',
  92. description: 'Failed to parse the docker-compose.yml file. Please check your repository URL and file path.',
  93. });
  94. } finally {
  95. setLoading(false);
  96. }
  97. };
  98. const handleCreateApp = async () => {
  99. if (!parseResult) return;
  100. setLoading(true);
  101. try {
  102. // Call the backend API to create the app and components
  103. const response = await fetch(`${API_URL}/apps/import/create`, {
  104. method: 'POST',
  105. headers: {
  106. 'Content-Type': 'application/json',
  107. 'Authorization': `Bearer ${localStorage.getItem('byop-auth')}`,
  108. },
  109. body: JSON.stringify({
  110. source_url: form.getFieldValue('source_url'),
  111. branch: form.getFieldValue('branch'),
  112. compose_path: form.getFieldValue('compose_path'),
  113. app_name: form.getFieldValue('app_name'),
  114. confirmed_app_name: parseResult.app_name,
  115. }),
  116. });
  117. if (!response.ok) {
  118. throw new Error('Failed to create app from compose');
  119. }
  120. const createdApp = await response.json();
  121. open?.({
  122. type: 'success',
  123. message: 'App Created Successfully',
  124. description: `Your app "${parseResult.app_name}" has been created with ${parseResult.services.length} components.`,
  125. });
  126. // Redirect to the new app's show page
  127. navigate(`/apps/show/${createdApp.id}`);
  128. } catch (error) {
  129. open?.({
  130. type: 'error',
  131. message: 'Creation Error',
  132. description: 'Failed to create the app. Please try again.',
  133. });
  134. } finally {
  135. setLoading(false);
  136. }
  137. };
  138. const renderImportForm = () => (
  139. <Card title="Import from docker-compose.yml" extra={
  140. <Button
  141. type="text"
  142. icon={<ArrowLeftOutlined />}
  143. onClick={() => navigate('/apps/create')}
  144. >
  145. Back to Create Options
  146. </Button>
  147. }>
  148. <Alert
  149. message="Import your existing multi-service application"
  150. description="Provide your Git repository details and we'll parse your docker-compose.yml to automatically create components for each service."
  151. type="info"
  152. showIcon
  153. style={{ marginBottom: 24 }}
  154. />
  155. <Form
  156. form={form}
  157. layout="vertical"
  158. onFinish={handleParseCompose}
  159. initialValues={{
  160. branch: 'main',
  161. compose_path: 'docker-compose.yml'
  162. }}
  163. >
  164. <Form.Item
  165. label="App Name (Optional)"
  166. name="app_name"
  167. help="Leave empty to use the repository name"
  168. >
  169. <Input placeholder="e.g., my-awesome-app" />
  170. </Form.Item>
  171. <Form.Item
  172. label="Git Repository URL"
  173. name="source_url"
  174. rules={[
  175. { required: true, message: 'Please enter the Git repository URL' },
  176. { type: 'url', message: 'Please enter a valid URL' }
  177. ]}
  178. >
  179. <Input
  180. prefix={<GitlabOutlined />}
  181. placeholder="https://github.com/username/repository.git"
  182. />
  183. </Form.Item>
  184. <Form.Item
  185. label="Branch"
  186. name="branch"
  187. rules={[{ required: true, message: 'Please enter the branch name' }]}
  188. >
  189. <Input placeholder="main" />
  190. </Form.Item>
  191. <Form.Item
  192. label="Path to Compose File"
  193. name="compose_path"
  194. rules={[{ required: true, message: 'Please enter the path to docker-compose.yml' }]}
  195. >
  196. <Input
  197. prefix={<FileTextOutlined />}
  198. placeholder="docker-compose.yml"
  199. />
  200. </Form.Item>
  201. <Form.Item>
  202. <Button
  203. type="primary"
  204. htmlType="submit"
  205. loading={loading}
  206. size="large"
  207. >
  208. Parse Compose File
  209. </Button>
  210. </Form.Item>
  211. </Form>
  212. </Card>
  213. );
  214. const renderReviewStep = () => (
  215. <Card title="Review Your Application">
  216. <Space direction="vertical" style={{ width: '100%' }} size="large">
  217. <Alert
  218. message={`We found ${parseResult?.services.length} services in your docker-compose.yml`}
  219. description="We will create a component for each service. Review the details below and click 'Create App and Components' to proceed."
  220. type="success"
  221. showIcon
  222. />
  223. <div>
  224. <Title level={4}>App Name: {parseResult?.app_name}</Title>
  225. <Divider />
  226. <Title level={5}>Services Found:</Title>
  227. <Space direction="vertical" style={{ width: '100%' }} size={16}>
  228. {parseResult?.services.map((service: ParsedService, index: number) => (
  229. <Card
  230. key={index}
  231. size="small"
  232. title={
  233. <Space>
  234. <Tag color="blue">{service.name}</Tag>
  235. <Text strong>Service: {service.name}</Text>
  236. </Space>
  237. }
  238. >
  239. <Space direction="vertical" size="small">
  240. {service.source === 'build' && service.build_context && (
  241. <Text>
  242. <strong>Source:</strong> Will be built from the <code>{service.build_context}</code> directory.
  243. {service.dockerfile && <span> Using <code>{service.dockerfile}</code></span>}
  244. </Text>
  245. )}
  246. {service.source === 'image' && service.image && (
  247. <Text>
  248. <strong>Source:</strong> Uses the public image <code>{service.image}</code>.
  249. </Text>
  250. )}
  251. {service.ports && service.ports.length > 0 && (
  252. <Text>
  253. <strong>Ports:</strong> {service.ports.join(', ')}
  254. </Text>
  255. )}
  256. {service.environment && Object.keys(service.environment).length > 0 && (
  257. <Text>
  258. <strong>Environment Variables:</strong> {Object.keys(service.environment).length} variables defined
  259. </Text>
  260. )}
  261. {service.volumes && service.volumes.length > 0 && (
  262. <Text>
  263. <strong>Volumes:</strong> {service.volumes.length} volume mappings
  264. </Text>
  265. )}
  266. </Space>
  267. </Card>
  268. ))}
  269. </Space>
  270. </div>
  271. <Space>
  272. <Button
  273. onClick={() => setCurrentStep(0)}
  274. disabled={loading}
  275. >
  276. Back to Edit
  277. </Button>
  278. <Button
  279. type="primary"
  280. size="large"
  281. loading={loading}
  282. onClick={handleCreateApp}
  283. icon={<CheckCircleOutlined />}
  284. >
  285. Create App and Components
  286. </Button>
  287. </Space>
  288. </Space>
  289. </Card>
  290. );
  291. return (
  292. <Create
  293. title="Import from docker-compose.yml"
  294. breadcrumb={false}
  295. headerButtons={[]}
  296. >
  297. <Space direction="vertical" style={{ width: '100%' }} size="large">
  298. <Steps current={currentStep} size="small">
  299. <Step title="Import Details" description="Repository and compose file info" />
  300. <Step title="Review & Confirm" description="Verify services and create app" />
  301. </Steps>
  302. <Spin spinning={loading}>
  303. {currentStep === 0 && renderImportForm()}
  304. {currentStep === 1 && renderReviewStep()}
  305. </Spin>
  306. </Space>
  307. </Create>
  308. );
  309. };