From c756abc1a254af70b8c9011a5088b212a7a87470 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Sat, 14 Sep 2024 13:59:23 +0200 Subject: [PATCH 01/33] Add embed option to share modal --- .../src/main/resources/diagram-resource.ts | 11 ++++- packages/shared/src/main/diagram-view.ts | 1 + .../modals/share-modal/share-modal.tsx | 46 ++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/server/src/main/resources/diagram-resource.ts b/packages/server/src/main/resources/diagram-resource.ts index 6db651ce..dd458372 100644 --- a/packages/server/src/main/resources/diagram-resource.ts +++ b/packages/server/src/main/resources/diagram-resource.ts @@ -6,9 +6,11 @@ import pdfFonts from 'pdfmake/build/vfs_fonts'; import { DiagramDTO } from 'shared/src/main/diagram-dto'; import { DiagramService } from '../services/diagram-service/diagram-service'; import { DiagramStorageFactory } from '../services/diagram-storage'; +import { ConversionService } from '../services/conversion-service/conversion-service'; export class DiagramResource { diagramService: DiagramService = new DiagramService(DiagramStorageFactory.getStorageService()); + conversionService: ConversionService = new ConversionService(); getDiagram = (req: Request, res: Response) => { const tokenValue: string = req.params.token; @@ -19,9 +21,14 @@ export class DiagramResource { if (/^[a-zA-Z0-9]+$/.test(tokenValue)) { this.diagramService .getDiagramByLink(tokenValue) - .then((diagram: DiagramDTO | undefined) => { + .then(async (diagram: DiagramDTO | undefined) => { if (diagram) { - res.json(diagram); + if (req.query.type === 'svg') { + const diagramSvg = await this.conversionService.convertToSvg(diagram.model); + res.send(diagramSvg.svg); + } else { + res.json(diagram); + } } else { res.status(404).send('Diagram not found'); } diff --git a/packages/shared/src/main/diagram-view.ts b/packages/shared/src/main/diagram-view.ts index 6d2b5e12..945a483f 100644 --- a/packages/shared/src/main/diagram-view.ts +++ b/packages/shared/src/main/diagram-view.ts @@ -3,4 +3,5 @@ export enum DiagramView { SEE_FEEDBACK = 'SEE_FEEDBACK', EDIT = 'EDIT', COLLABORATE = 'COLLABORATE', + EMBED = 'EMBED', } diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index 014805f5..36cd3d44 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -48,6 +48,12 @@ class ShareModalComponent extends Component { state = getInitialState(); getLinkForView = () => { + if (LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED) { + return `![${ + this.props.diagram ? this.props.diagram.title : 'Diagram' + }](${DEPLOYMENT_URL}/api/diagrams/${LocalStorageRepository.getLastPublishedToken()}?type=svg)`; + } + return `${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${LocalStorageRepository.getLastPublishedType()}`; }; @@ -63,6 +69,9 @@ class ShareModalComponent extends Component { case DiagramView.COLLABORATE: innerMessage = 'collaborate'; break; + case DiagramView.EMBED: + innerMessage = 'embed'; + break; } return `${innerMessage}`; }; @@ -74,7 +83,16 @@ class ShareModalComponent extends Component { }; copyLink = (displayToast = false) => { - const link = this.getLinkForView(); + let link = this.getLinkForView(); + + if (LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED) { + link = + link + + `\n[Give Feedback](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${ + DiagramView.GIVE_FEEDBACK + })`; + } + navigator.clipboard.writeText(link); if (displayToast) this.displayToast(); }; @@ -90,7 +108,7 @@ class ShareModalComponent extends Component { this.setState({ token }, () => { LocalStorageRepository.setLastPublishedToken(token); LocalStorageRepository.setLastPublishedType(this.state.view); - if (this.state.view === 'COLLABORATE') { + if (this.state.view === DiagramView.COLLABORATE) { window.location.href = this.getLinkForView() + '¬ifyUser=true'; } this.copyLink(true); @@ -200,6 +218,17 @@ class ShareModalComponent extends Component { Collaborate +
+ +
@@ -222,6 +251,19 @@ class ShareModalComponent extends Component { Copy Link + {LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED && ( + + + + + )} )} From 4447d437aa3f179f129bb1288be6f8285867e8fb Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Mon, 16 Sep 2024 23:01:01 +0200 Subject: [PATCH 02/33] Adjust share modal --- .../modals/share-modal/share-modal.tsx | 76 ++++++++----------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index 36cd3d44..4b6de273 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -49,9 +49,14 @@ class ShareModalComponent extends Component { getLinkForView = () => { if (LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED) { - return `![${ - this.props.diagram ? this.props.diagram.title : 'Diagram' - }](${DEPLOYMENT_URL}/api/diagrams/${LocalStorageRepository.getLastPublishedToken()}?type=svg)`; + return ( + `![${ + this.props.diagram ? this.props.diagram.title : 'Diagram' + }](${DEPLOYMENT_URL}/api/diagrams/${LocalStorageRepository.getLastPublishedToken()}?type=svg)` + + `\n[Give Feedback](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${ + DiagramView.GIVE_FEEDBACK + }` + ); } return `${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${LocalStorageRepository.getLastPublishedType()}`; @@ -83,16 +88,7 @@ class ShareModalComponent extends Component { }; copyLink = (displayToast = false) => { - let link = this.getLinkForView(); - - if (LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED) { - link = - link + - `\n[Give Feedback](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${ - DiagramView.GIVE_FEEDBACK - })`; - } - + const link = this.getLinkForView(); navigator.clipboard.writeText(link); if (displayToast) this.displayToast(); }; @@ -174,7 +170,7 @@ class ShareModalComponent extends Component {
-
+
-
+
-
+
-
+
-
+
@@ -237,13 +233,18 @@ class ShareModalComponent extends Component { Recently shared Diagram: {!this.state.token ? ( - + ) : ( )} @@ -251,19 +252,6 @@ class ShareModalComponent extends Component { Copy Link - {LocalStorageRepository.getLastPublishedType() === DiagramView.EMBED && ( - - - - - )} )} From a6a7d869b471938dbe90755c5c6b9529ba43ab5f Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 19 Sep 2024 20:43:33 +0200 Subject: [PATCH 03/33] Adjust embedding link --- .../src/main/components/modals/share-modal/share-modal.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index 4b6de273..b9361ad5 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -53,9 +53,7 @@ class ShareModalComponent extends Component { `![${ this.props.diagram ? this.props.diagram.title : 'Diagram' }](${DEPLOYMENT_URL}/api/diagrams/${LocalStorageRepository.getLastPublishedToken()}?type=svg)` + - `\n[Give Feedback](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${ - DiagramView.GIVE_FEEDBACK - }` + `\n[Edit a copy](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${DiagramView.EDIT}` ); } From 206c99308ff9eda586d237249c516b5fc05d843e Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 19 Sep 2024 21:24:20 +0200 Subject: [PATCH 04/33] Fix typo in embedding link --- .../src/main/components/modals/share-modal/share-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index b9361ad5..f6e4d921 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -53,7 +53,7 @@ class ShareModalComponent extends Component { `![${ this.props.diagram ? this.props.diagram.title : 'Diagram' }](${DEPLOYMENT_URL}/api/diagrams/${LocalStorageRepository.getLastPublishedToken()}?type=svg)` + - `\n[Edit a copy](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${DiagramView.EDIT}` + `\n[Edit a copy](${DEPLOYMENT_URL}/${LocalStorageRepository.getLastPublishedToken()}?view=${DiagramView.EDIT})` ); } From 8dd423610db8fb25c57e42584cad3d16b9020fc2 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 26 Sep 2024 17:02:04 +0200 Subject: [PATCH 05/33] Adjust content type to fix embedding not being displayed --- packages/server/src/main/resources/diagram-resource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/main/resources/diagram-resource.ts b/packages/server/src/main/resources/diagram-resource.ts index dd458372..4dd91fc2 100644 --- a/packages/server/src/main/resources/diagram-resource.ts +++ b/packages/server/src/main/resources/diagram-resource.ts @@ -25,6 +25,7 @@ export class DiagramResource { if (diagram) { if (req.query.type === 'svg') { const diagramSvg = await this.conversionService.convertToSvg(diagram.model); + res.setHeader('Content-Type', 'image/svg+xml'); res.send(diagramSvg.svg); } else { res.json(diagram); From 5bd020497e75da3884adf3bcc406d096d0cba332 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Sun, 29 Sep 2024 23:54:01 +0200 Subject: [PATCH 06/33] Make UI change for deployment debugging --- .../src/main/components/modals/share-modal/share-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index f6e4d921..1cf5e71d 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -149,7 +149,7 @@ class ShareModalComponent extends Component { return ( <> - Share + Share Test <> From 313bf530ce111a12d8742e47d068c0cab557191b Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 3 Oct 2024 21:53:57 +0200 Subject: [PATCH 07/33] Add white background to svg --- .../src/main/resources/diagram-resource.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/server/src/main/resources/diagram-resource.ts b/packages/server/src/main/resources/diagram-resource.ts index 4dd91fc2..f62f6872 100644 --- a/packages/server/src/main/resources/diagram-resource.ts +++ b/packages/server/src/main/resources/diagram-resource.ts @@ -24,15 +24,20 @@ export class DiagramResource { .then(async (diagram: DiagramDTO | undefined) => { if (diagram) { if (req.query.type === 'svg') { - const diagramSvg = await this.conversionService.convertToSvg(diagram.model); + const diagramSvg = (await this.conversionService.convertToSvg(diagram.model)).svg; + const diagramSvgWhiteBackground = diagramSvg.replace( + /]*)>/, + '', + ); + res.setHeader('Content-Type', 'image/svg+xml'); - res.send(diagramSvg.svg); - } else { - res.json(diagram); + return res.send(diagramSvgWhiteBackground); } - } else { - res.status(404).send('Diagram not found'); + + return res.json(diagram); } + + return res.status(404).send('Diagram not found'); }) .catch(() => res.status(503).send('Error occurred')); } else { From 18223c97a9faf167c803f7ec16106aa5ca07d2f5 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 3 Oct 2024 23:55:29 +0200 Subject: [PATCH 08/33] Remove change for debugging purposes --- .../src/main/components/modals/share-modal/share-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index 1cf5e71d..f6e4d921 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -149,7 +149,7 @@ class ShareModalComponent extends Component { return ( <> - Share Test + Share <> From 4aef016f204aead452974b719cfe905122e7a329 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Fri, 22 Nov 2024 14:00:59 +0100 Subject: [PATCH 09/33] Adjust README --- README.md | 62 +++++++++++++++++++----------------- docs/images/ShareDialog.png | Bin 10439 -> 24073 bytes 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 77ff7509..50d81c8a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Apollon Standalone is the Standalone version of the [Apollon Editor](https://github.com/ls1intum/Apollon) There are two variants how you can use this editor: + 1. As web application which only runs in the users environment (modeling functionality). 2. With an application server which enables some extra features, like sharing of diagrams. @@ -20,42 +21,43 @@ All you have to do is go to the [URL](https://apollon.ase.in.tum.de/) and start The user interface of Apollon is simple to use. It works just like any other office and drawing tool that most users are familiar with. -- Select the diagram type you want to draw by clicking on the `File > New` menu. This selection determines the availability of elements that the user can use while drawing their diagram, making it easier for users who are newly introduced to modeling. -- Adding the element is as easy as dragging it from the elements menu and dropping it to the canvas. So is drawing the connection between them, simply drag and connect two or multiple elements. -- The layout of the connection is drawn automatically by the editor. If you want to manually layout it, use the existing waypoints features. -- Edit or style the text or change the colors of any elements by double-clicking on them. An easy-to-use menu will allow you to do so. -- Use keyboard shortcuts to copy, paste, delete and move the elements throughout the canvas. -- Change the theme of the editor by clicking on the dark/light mode switch. +- Select the diagram type you want to draw by clicking on the `File > New` menu. This selection determines the availability of elements that the user can use while drawing their diagram, making it easier for users who are newly introduced to modeling. +- Adding the element is as easy as dragging it from the elements menu and dropping it to the canvas. So is drawing the connection between them, simply drag and connect two or multiple elements. +- The layout of the connection is drawn automatically by the editor. If you want to manually layout it, use the existing waypoints features. +- Edit or style the text or change the colors of any elements by double-clicking on them. An easy-to-use menu will allow you to do so. +- Use keyboard shortcuts to copy, paste, delete and move the elements throughout the canvas. +- Change the theme of the editor by clicking on the dark/light mode switch. ### Import and Export your diagrams Users can easily import the existing Apollon diagram to any editor that uses the Apollon library and continue editing. -![Import Diagram](/docs/images/Import.gif "Import Diagram") +![Import Diagram](/docs/images/Import.gif 'Import Diagram') -Exporting the diagrams is as easy as importing them. +Exporting the diagrams is as easy as importing them. Click on `File > Export` and select the format of the diagram to be exported as. Currently, Apollon standalone supports five different formats: `SVG`, `PNG (White Background)`, `PNG (Transparent Background)`, `JSON`, and `PDF`. -![Export Diagram](/docs/images/Export.png "Export Diagram") +![Export Diagram](/docs/images/Export.png 'Export Diagram') ### Create diagram from template -Users in Apollon Standalone can also create a diagram from a template if they do not want to draw a diagram from scratch. +Users in Apollon Standalone can also create a diagram from a template if they do not want to draw a diagram from scratch. To do that, all they have to do is click on `File > Start from Template` and select one of the templates from the list of available templates. -![Start from Template](/docs/images/StartFromTemplate.gif "Start from Template") +![Start from Template](/docs/images/StartFromTemplate.gif 'Start from Template') ### Share your diagram with others Users can share the diagram in Apollon Standalone in four different types. -- `Edit`: In this mode of sharing, the user will be able to make changes to the shared diagram. -- `Give Feedback`: In this mode of sharing, the user will not be able to make changes to the shared diagram, but can only provide feedback to it. -- `See Feedback`: In this mode of sharing, the user can view feedback provided to the shared diagram. -- `Collaborate`: In this mode of sharing, users joining the collaboration session will be able to work on the diagram collaboratively with other users. +- `Edit`: In this mode of sharing, the user will be able to make changes to the shared diagram. +- `Collaborate`: In this mode of sharing, users joining the collaboration session will be able to work on the diagram collaboratively with other users. +- `Embed`: In this mode of sharing, the user embeds the diagram in a Git issue/pull request. The embedding displays the latest version of the diagram. +- `Give Feedback`: In this mode of sharing, the user will not be able to make changes to the shared diagram, but can only provide feedback to it. +- `See Feedback`: In this mode of sharing, the user can view feedback provided to the shared diagram. -![Real-time collaboration](/docs/images/ShareDialog.png "Real-time collaboration") +![Real-time collaboration](/docs/images/ShareDialog.png 'Real-time collaboration') ### Collaborate in real-time @@ -63,7 +65,7 @@ Apollon Standalone can be used as a collaborative modeling canvas, where multipl Any changes made by one user will be visible throughout the canvas of all other users that are in collaboration sessions in real-time. Active elements that are interacted with by users in a session are highlighted in the canvas. -![Real-time collaboration](/docs/images/RealTimeCollaboration.gif "Real-time collaboration") +![Real-time collaboration](/docs/images/RealTimeCollaboration.gif 'Real-time collaboration') ## Build the application @@ -94,16 +96,17 @@ page application will be loaded. ### Web application + application server There are two variants to set this up: + 1. Manual on a linux vm 2. In a docker container - #### Manual setup (Installation of application server on linux machine) > [!IMPORTANT] > Please make sure if there is any requirements regarding additional dependencies to build the node canvas package for -your operating system! You can find instructions for installing these dependencies here: +> your operating system! You can find instructions for installing these dependencies here: > https://github.com/Automattic/node-canvas#compiling + ``` # clone the repository git clone https://github.com/ls1intum/Apollon_standalone @@ -141,16 +144,17 @@ chown apollon_standalone path/to/diagrams ``` Add the path to the created directory to: + - the cronjob in delete-stale-diagrams.cronjob.txt - in packages/server/src/main/constants.ts #### Install as a service -Configure the apollon_standalone.service file so that the paths +Configure the apollon_standalone.service file so that the paths match the paths to your installation folder ``` -# After adjusting the service file, copy the service file apollon_standalone.service +# After adjusting the service file, copy the service file apollon_standalone.service # into the /etc/systemd/system directory service apollon_standalone start cp apollon_standalone.service /etc/systemd/system/ @@ -158,7 +162,7 @@ cp apollon_standalone.service /etc/systemd/system/ cd path/to/application/build/server chmod +x server.js -# Start the service +# Start the service sudo service apollon_standalone start # Status of the service @@ -166,6 +170,7 @@ service apollon_standalone status ``` Error codes on server start: + - (code=exited, status=217/USER) -> apollon_standalone user does not exist - (code=exited, status=203/USER) -> script not executable @@ -201,7 +206,7 @@ git clone https://github.com/ls1intum/Apollon_standalone # build docker container docker build -t apollon_standalone . -run docker container +run docker container docker run -d --name apollon_standalone -p 8080:8080 apollon_standalone # build the web application and the application server @@ -225,7 +230,6 @@ To use Redis, set the environment variable `APOLLON_REDIS_URL` to the URL of the > [!IMPORTANT] > Apollon Standalone requires the Redis JSON module to be enabled. [Read the documents](https://redis.io/docs/latest/develop/data-types/json/) to learn how to enable the JSON module. - ```bash APOLLON_REDIS_URL=redis://[[username]:[password]@][host][:port] ``` @@ -313,15 +317,15 @@ npm run update While developing the Standalone project, it is often required to make changes in the Apollon project. This can be achieved by executing the following workflow. -1. In the *Apollon* project: Generate a symlink by executing `npm link` command. -2. In the *Standalone* project: Link the generated symlink of Apollon *(from step 1)* by executing `npm link "@ls1intum/apollon"` command. +1. In the _Apollon_ project: Generate a symlink by executing `npm link` command. +2. In the _Standalone_ project: Link the generated symlink of Apollon _(from step 1)_ by executing `npm link "@ls1intum/apollon"` command. For more information please refer to the [documentation](https://docs.npmjs.com/cli/v9/commands/npm-link) of npm. -> ***Note***: While making changes in the *Apollon* project, for the changes to get reflected in *Standalone*, execute the following workflow: +> **_Note_**: While making changes in the _Apollon_ project, for the changes to get reflected in _Standalone_, execute the following workflow: > -> - Recompile the Apollon project by executing `npm run prepare` -> - Rebuild the Standalone project by executing `npm run build` +> - Recompile the Apollon project by executing `npm run prepare` +> - Rebuild the Standalone project by executing `npm run build` ### Using Redis in Development diff --git a/docs/images/ShareDialog.png b/docs/images/ShareDialog.png index 689a06b435b8f6d774e6469a7c03b9e05b42abcf..8a1b057d162c30634d32d08ba8f182507b3e4adb 100644 GIT binary patch literal 24073 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 Date: Mon, 25 Nov 2024 15:22:56 +0100 Subject: [PATCH 10/33] Fix disappearing versions --- .../modals/create-version-modal/create-version-modal.tsx | 3 ++- .../src/main/components/modals/share-modal/share-modal.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx index bb6d4f67..a8224329 100644 --- a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx +++ b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx @@ -4,7 +4,7 @@ import { ModalContentProps } from '../application-modal-types'; import { toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { selectDiagram, updateDiagramThunk } from '../../../services/diagram/diagramSlice'; +import { selectDiagram, setCreateNewEditor, updateDiagramThunk } from '../../../services/diagram/diagramSlice'; import { LocalStorageRepository } from '../../../services/local-storage/local-storage-repository'; import { displayError } from '../../../services/error-management/errorManagementSlice'; import { DiagramRepository } from '../../../services/diagram/diagram-repository'; @@ -42,6 +42,7 @@ export const CreateVersionModal: React.FC = ({ close }) => { DiagramRepository.publishDiagramVersionOnServer(diagramCopy, token) .then((res) => { dispatch(updateDiagramThunk(res.diagram)); + dispatch(setCreateNewEditor(true)); dispatch(setDisplayUnpublishedVersion(false)); LocalStorageRepository.setLastPublishedToken(res.diagramToken); displayToast(); diff --git a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx index 87fbb57b..d68869e8 100644 --- a/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx +++ b/packages/webapp/src/main/components/modals/share-modal/share-modal.tsx @@ -11,7 +11,11 @@ import { InfoCircle } from 'react-bootstrap-icons'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { displayError } from '../../../services/error-management/errorManagementSlice'; import { useNavigate } from 'react-router-dom'; -import { setDisplayUnpublishedVersion, updateDiagramThunk } from '../../../services/diagram/diagramSlice'; +import { + setCreateNewEditor, + setDisplayUnpublishedVersion, + updateDiagramThunk, +} from '../../../services/diagram/diagramSlice'; import { selectDisplaySidebar, toggleSidebar } from '../../../services/version-management/versionManagementSlice'; export const ShareModal: React.FC = ({ close }) => { @@ -99,6 +103,7 @@ export const ShareModal: React.FC = ({ close }) => { DiagramRepository.publishDiagramVersionOnServer(diagramCopy, diagram.token) .then((res) => { dispatch(updateDiagramThunk(res.diagram)); + dispatch(setCreateNewEditor(true)); dispatch(setDisplayUnpublishedVersion(false)); token = res.diagramToken; }) From bd219b7096122cc99d53aebf71b3c5f0a79be77d Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 28 Nov 2024 13:22:41 +0100 Subject: [PATCH 11/33] Attempt to fix bamboo --- .../modals/create-version-modal/create-version-modal.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx index bb6d4f67..90059840 100644 --- a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx +++ b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx @@ -10,6 +10,8 @@ import { displayError } from '../../../services/error-management/errorManagement import { DiagramRepository } from '../../../services/diagram/diagram-repository'; import { setDisplayUnpublishedVersion } from '../../../services/diagram/diagramSlice'; +console.log('yes'); + export const CreateVersionModal: React.FC = ({ close }) => { const dispatch = useAppDispatch(); const diagram = useAppSelector(selectDiagram); From b52864b454367bffdf6b39d1ead19672231858d9 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Thu, 28 Nov 2024 13:33:49 +0100 Subject: [PATCH 12/33] Attempt to fix bamboo --- .../modals/create-version-modal/create-version-modal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx index 90059840..bb6d4f67 100644 --- a/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx +++ b/packages/webapp/src/main/components/modals/create-version-modal/create-version-modal.tsx @@ -10,8 +10,6 @@ import { displayError } from '../../../services/error-management/errorManagement import { DiagramRepository } from '../../../services/diagram/diagram-repository'; import { setDisplayUnpublishedVersion } from '../../../services/diagram/diagramSlice'; -console.log('yes'); - export const CreateVersionModal: React.FC = ({ close }) => { const dispatch = useAppDispatch(); const diagram = useAppSelector(selectDiagram); From d4fdf2b3468d6c64e05d5d1aa453e2d7f20da3de Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 29 Nov 2024 10:25:48 +0100 Subject: [PATCH 13/33] create prod compose --- Dockerfile.redis | 3 ++- docker-compose.prod.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docker-compose.prod.yml diff --git a/Dockerfile.redis b/Dockerfile.redis index 09d8f44c..8c6a997d 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -8,9 +8,10 @@ RUN apk add --no-cache \ giflib-dev +ARG APOLLON_REDIS_URL="redis://localhost:6379" ARG DEPLOYMENT_URL="http://localhost:8080" -ENV APOLLON_REDIS_URL="" +ENV APOLLON_REDIS_URL=${APOLLON_REDIS_URL} ENV DEPLOYMENT_URL=${DEPLOYMENT_URL} WORKDIR /app diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..69e0c971 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,40 @@ +version: '3.8' + +services: + redis: + image: redis/redis-stack-server:7.4.0-v1 + container_name: apollon-redis + volumes: + - ./redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - apollon-network + + apollon_standalone: + build: + context: . + dockerfile: Dockerfile.redis + args: + - DEPLOYMENT_URL=${DEPLOYMENT_URL} + container_name: apollon-standalone + environment: + - APOLLON_REDIS_URL=redis://apollon_redis:6379 + - APOLLON_REDIS_DIAGRAM_TTL=${APOLLON_REDIS_DIAGRAM_TTL} + - DEPLOYMENT_URL=${DEPLOYMENT_URL} + restart: unless-stopped + expose: + - "8080" + depends_on: + redis: + condition: service_healthy + networks: + - apollon-network + +networks: + apollon-network: + driver: bridge From 380a3977a1338dce6d7c1f904070788d26681fa2 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 29 Nov 2024 10:44:21 +0100 Subject: [PATCH 14/33] add workflows --- .github/workflows/build-and-push-docker.yml | 75 +++++++++++++++++++++ .github/workflows/dev.yml | 17 +++++ 2 files changed, 92 insertions(+) create mode 100644 .github/workflows/build-and-push-docker.yml create mode 100644 .github/workflows/dev.yml diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml new file mode 100644 index 00000000..67325cb7 --- /dev/null +++ b/.github/workflows/build-and-push-docker.yml @@ -0,0 +1,75 @@ +name: Build Docker Image + +on: + workflow_call: + outputs: + apollon_standalone_image_tag: + description: "The tag of the apollon standalone image that was built" + value: ${{ jobs.build.outputs.apollon_standalone_image_tag }} + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: ./Dockerfile.redis + image: ghcr.io/ls1intum/Apollon_standalone/apollon-standalone + context: . + path: apollon-standalone + outputs: + apollon_standalone_image_tag: "${{ steps.output-tag-apollon-standalone.outputs.apollon_standalone_image_tag }}" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Install Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ matrix.image }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + + - name: Build and push Docker Image + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + - id: output-tag-apollon-standalone + run: | + if [[ "${{ matrix.path }}" == "apollon-standalone" ]] && [[ "${{ steps.changed-files.outputs.any_changed }}" == "true" ]]; then + echo "apollon_standalone_image_tag=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ matrix.path }}" == "server" ]]; then + echo "apollon_standalone_image_tag=latest" >> "$GITHUB_OUTPUT" + fi \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 00000000..334df0f6 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,17 @@ +name: Build and Deploy to Dev + +on: + pull_request: + +jobs: + build-dev-container: + uses: ./.github/workflows/build-and-push-docker.yml + secrets: inherit + # deploy-dev-container: + # needs: build-dev-container + # uses: ./.github/workflows/deploy_docker.yml + # secrets: inherit + # with: + # environment: Dev + # server_image_tag: "${{ needs.build-dev-container.outputs.server_image_tag }}" + # client_image_tag: "${{ needs.build-dev-container.outputs.client_image_tag }}" \ No newline at end of file From 5dda4dd4accd487e5dcc708b40b5d5ac94110ce1 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 29 Nov 2024 11:15:42 +0100 Subject: [PATCH 15/33] add deploy workflow --- .github/workflows/deploy-docker.yml | 80 +++++++++++++++++++++++++++++ .github/workflows/dev.yml | 15 +++--- Dockerfile.redis | 6 ++- docker-compose.prod.yml | 6 +-- 4 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/deploy-docker.yml diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml new file mode 100644 index 00000000..d3a917c5 --- /dev/null +++ b/.github/workflows/deploy-docker.yml @@ -0,0 +1,80 @@ +name: Deploy Docker Image + +on: + workflow_call: + inputs: + environment: + required: true + type: string + apollon_standalone_image_tag: + default: "latest" + type: string + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: ${{ inputs.environment }} + url: 'https://${{ vars.SERVER_HOST }}' + steps: + - name: SSH to VM and Execute Docker-Compose Down + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} + script: | + docker compose -f docker-compose.prod.yml --env-file=.env.prod down --remove-orphans --rmi all + + - name: checkout + uses: actions/checkout@v4 + + - name: Copy Docker Compose File From Repo to VM Host + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} + source: "./docker-compose.prod.yml" + target: /home/${{ vars.VM_USERNAME }} + + - name: SSH to VM and create .env.prod file + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} + script: | + touch .env.prod + + echo "ENVIRONMENT=${{ vars.ENVIRONMENT }}" > .env.prod + + echo "DEPLOYMENT_URL=${{ vars.DEPLOYMENT_URL }}" > .env.prod + echo "APOLLON_REDIS_DIAGRAM_TTL=${{ vars.APOLLON_REDIS_DIAGRAM_TTL }}" >> .env.prod + + echo "APOLLON_STANDALONE_IMAGE_TAG=${{ inputs.apollon_standalone_image_tag }}" >> .env.prod + + - name: SSH to VM and Execute Docker-Compose Up + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ vars.VM_HOST }} + username: ${{ vars.VM_USERNAME }} + key: ${{ secrets.VM_SSH_PRIVATE_KEY }} + proxy_host: ${{ vars.DEPLOYMENT_GATEWAY_HOST }} + proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }} + proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} + proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} + script: | + docker compose -f docker-compose.prod.yml --env-file=.env.prod up --pull=always -d \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 334df0f6..b51025e4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -7,11 +7,10 @@ jobs: build-dev-container: uses: ./.github/workflows/build-and-push-docker.yml secrets: inherit - # deploy-dev-container: - # needs: build-dev-container - # uses: ./.github/workflows/deploy_docker.yml - # secrets: inherit - # with: - # environment: Dev - # server_image_tag: "${{ needs.build-dev-container.outputs.server_image_tag }}" - # client_image_tag: "${{ needs.build-dev-container.outputs.client_image_tag }}" \ No newline at end of file + deploy-dev-container: + needs: build-dev-container + uses: ./.github/workflows/deploy-docker.yml + secrets: inherit + with: + environment: Dev + apollon_standalone_image_tag: "${{ needs.build-dev-container.outputs.apollon_standalone_image_tag }}" \ No newline at end of file diff --git a/Dockerfile.redis b/Dockerfile.redis index 8c6a997d..9ccec61d 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -8,11 +8,13 @@ RUN apk add --no-cache \ giflib-dev -ARG APOLLON_REDIS_URL="redis://localhost:6379" ARG DEPLOYMENT_URL="http://localhost:8080" +ARG APOLLON_REDIS_URL="redis://localhost:6379" +ARG APOLLON_REDIS_DIAGRAM_TTL=3600 -ENV APOLLON_REDIS_URL=${APOLLON_REDIS_URL} ENV DEPLOYMENT_URL=${DEPLOYMENT_URL} +ENV APOLLON_REDIS_URL=${APOLLON_REDIS_URL} +ENV APOLLON_REDIS_DIAGRAM_TTL=${APOLLON_REDIS_DIAGRAM_TTL} WORKDIR /app diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 69e0c971..060e2e3a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,11 +16,7 @@ services: - apollon-network apollon_standalone: - build: - context: . - dockerfile: Dockerfile.redis - args: - - DEPLOYMENT_URL=${DEPLOYMENT_URL} + image: "ghcr.io/ls1intum/Apollon_standalone/apollon-standalone:${APOLLON_STANDALONE_IMAGE_TAG}" container_name: apollon-standalone environment: - APOLLON_REDIS_URL=redis://apollon_redis:6379 From 8deb3c72c7d0080a49268716a92ba003d28f8dae Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 29 Nov 2024 19:00:13 +0100 Subject: [PATCH 16/33] change port --- Dockerfile.redis | 2 +- README.md | 2 +- packages/webapp/webpack/webpack.common.js | 2 +- packages/webapp/webpack/webpack.dev.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile.redis b/Dockerfile.redis index 9ccec61d..e3b0bd49 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -10,7 +10,7 @@ RUN apk add --no-cache \ ARG DEPLOYMENT_URL="http://localhost:8080" ARG APOLLON_REDIS_URL="redis://localhost:6379" -ARG APOLLON_REDIS_DIAGRAM_TTL=3600 +ARG APOLLON_REDIS_DIAGRAM_TTL="30d" ENV DEPLOYMENT_URL=${DEPLOYMENT_URL} ENV APOLLON_REDIS_URL=${APOLLON_REDIS_URL} diff --git a/README.md b/README.md index 77ff7509..a575bc0d 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ mkdir diagrams # start webpack dev server npm start -# accessible via localhost:8888 (webpack dev server with proxy to application server) +# accessible via localhost:8080 (webpack dev server with proxy to application server) # accesible via localhost:8080 (application server with static files) ``` diff --git a/packages/webapp/webpack/webpack.common.js b/packages/webapp/webpack/webpack.common.js index 875fdfc2..14c8b68c 100644 --- a/packages/webapp/webpack/webpack.common.js +++ b/packages/webapp/webpack/webpack.common.js @@ -77,7 +77,7 @@ module.exports = { }), new webpack.DefinePlugin({ 'process.env.APPLICATION_SERVER_VERSION': JSON.stringify(process.env.APPLICATION_SERVER_VERSION || true), - 'process.env.DEPLOYMENT_URL': JSON.stringify(process.env.DEPLOYMENT_URL || 'http://localhost:8888'), + 'process.env.DEPLOYMENT_URL': JSON.stringify(process.env.DEPLOYMENT_URL || 'http://localhost:8080'), 'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN || null), 'process.env.POSTHOG_HOST': JSON.stringify(process.env.POSTHOG_HOST || null), 'process.env.POSTHOG_KEY': JSON.stringify(process.env.POSTHOG_KEY || null), diff --git a/packages/webapp/webpack/webpack.dev.js b/packages/webapp/webpack/webpack.dev.js index e72e66cb..ea22ad23 100644 --- a/packages/webapp/webpack/webpack.dev.js +++ b/packages/webapp/webpack/webpack.dev.js @@ -17,7 +17,7 @@ module.exports = merge(common, { devServer: { static: path.join(__dirname, '../../build/webapp'), host: '0.0.0.0', - port: 8888, + port: 8080, proxy: [ { context: ['/'], From 027fd16757521c6085788402774d3f65ad48dff8 Mon Sep 17 00:00:00 2001 From: Faris Demirovic Date: Sat, 30 Nov 2024 20:40:30 +0100 Subject: [PATCH 17/33] Format README, remove trailing backslash from example DEPLOYMENT_URL --- README.md | 63 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a575bc0d..80e2d930 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Apollon Standalone is the Standalone version of the [Apollon Editor](https://github.com/ls1intum/Apollon) There are two variants how you can use this editor: + 1. As web application which only runs in the users environment (modeling functionality). 2. With an application server which enables some extra features, like sharing of diagrams. @@ -20,42 +21,42 @@ All you have to do is go to the [URL](https://apollon.ase.in.tum.de/) and start The user interface of Apollon is simple to use. It works just like any other office and drawing tool that most users are familiar with. -- Select the diagram type you want to draw by clicking on the `File > New` menu. This selection determines the availability of elements that the user can use while drawing their diagram, making it easier for users who are newly introduced to modeling. -- Adding the element is as easy as dragging it from the elements menu and dropping it to the canvas. So is drawing the connection between them, simply drag and connect two or multiple elements. -- The layout of the connection is drawn automatically by the editor. If you want to manually layout it, use the existing waypoints features. -- Edit or style the text or change the colors of any elements by double-clicking on them. An easy-to-use menu will allow you to do so. -- Use keyboard shortcuts to copy, paste, delete and move the elements throughout the canvas. -- Change the theme of the editor by clicking on the dark/light mode switch. +- Select the diagram type you want to draw by clicking on the `File > New` menu. This selection determines the availability of elements that the user can use while drawing their diagram, making it easier for users who are newly introduced to modeling. +- Adding the element is as easy as dragging it from the elements menu and dropping it to the canvas. So is drawing the connection between them, simply drag and connect two or multiple elements. +- The layout of the connection is drawn automatically by the editor. If you want to manually layout it, use the existing waypoints features. +- Edit or style the text or change the colors of any elements by double-clicking on them. An easy-to-use menu will allow you to do so. +- Use keyboard shortcuts to copy, paste, delete and move the elements throughout the canvas. +- Change the theme of the editor by clicking on the dark/light mode switch. ### Import and Export your diagrams Users can easily import the existing Apollon diagram to any editor that uses the Apollon library and continue editing. -![Import Diagram](/docs/images/Import.gif "Import Diagram") +![Import Diagram](/docs/images/Import.gif 'Import Diagram') -Exporting the diagrams is as easy as importing them. +Exporting the diagrams is as easy as importing them. Click on `File > Export` and select the format of the diagram to be exported as. Currently, Apollon standalone supports five different formats: `SVG`, `PNG (White Background)`, `PNG (Transparent Background)`, `JSON`, and `PDF`. -![Export Diagram](/docs/images/Export.png "Export Diagram") +![Export Diagram](/docs/images/Export.png 'Export Diagram') ### Create diagram from template -Users in Apollon Standalone can also create a diagram from a template if they do not want to draw a diagram from scratch. +Users in Apollon Standalone can also create a diagram from a template if they do not want to draw a diagram from scratch. To do that, all they have to do is click on `File > Start from Template` and select one of the templates from the list of available templates. -![Start from Template](/docs/images/StartFromTemplate.gif "Start from Template") +![Start from Template](/docs/images/StartFromTemplate.gif 'Start from Template') ### Share your diagram with others Users can share the diagram in Apollon Standalone in four different types. -- `Edit`: In this mode of sharing, the user will be able to make changes to the shared diagram. -- `Give Feedback`: In this mode of sharing, the user will not be able to make changes to the shared diagram, but can only provide feedback to it. -- `See Feedback`: In this mode of sharing, the user can view feedback provided to the shared diagram. -- `Collaborate`: In this mode of sharing, users joining the collaboration session will be able to work on the diagram collaboratively with other users. +- `Edit`: In this mode of sharing, the user will be able to make changes to the shared diagram. +- `Give Feedback`: In this mode of sharing, the user will not be able to make changes to the shared diagram, but can only provide feedback to it. +- `See Feedback`: In this mode of sharing, the user can view feedback provided to the shared diagram. +- `Collaborate`: In this mode of sharing, users joining the collaboration session will be able to work on the diagram collaboratively with other users. -![Real-time collaboration](/docs/images/ShareDialog.png "Real-time collaboration") +![Real-time collaboration](/docs/images/ShareDialog.png 'Real-time collaboration') ### Collaborate in real-time @@ -63,7 +64,7 @@ Apollon Standalone can be used as a collaborative modeling canvas, where multipl Any changes made by one user will be visible throughout the canvas of all other users that are in collaboration sessions in real-time. Active elements that are interacted with by users in a session are highlighted in the canvas. -![Real-time collaboration](/docs/images/RealTimeCollaboration.gif "Real-time collaboration") +![Real-time collaboration](/docs/images/RealTimeCollaboration.gif 'Real-time collaboration') ## Build the application @@ -94,16 +95,17 @@ page application will be loaded. ### Web application + application server There are two variants to set this up: + 1. Manual on a linux vm 2. In a docker container - #### Manual setup (Installation of application server on linux machine) > [!IMPORTANT] > Please make sure if there is any requirements regarding additional dependencies to build the node canvas package for -your operating system! You can find instructions for installing these dependencies here: +> your operating system! You can find instructions for installing these dependencies here: > https://github.com/Automattic/node-canvas#compiling + ``` # clone the repository git clone https://github.com/ls1intum/Apollon_standalone @@ -141,16 +143,17 @@ chown apollon_standalone path/to/diagrams ``` Add the path to the created directory to: + - the cronjob in delete-stale-diagrams.cronjob.txt - in packages/server/src/main/constants.ts #### Install as a service -Configure the apollon_standalone.service file so that the paths +Configure the apollon_standalone.service file so that the paths match the paths to your installation folder ``` -# After adjusting the service file, copy the service file apollon_standalone.service +# After adjusting the service file, copy the service file apollon_standalone.service # into the /etc/systemd/system directory service apollon_standalone start cp apollon_standalone.service /etc/systemd/system/ @@ -158,7 +161,7 @@ cp apollon_standalone.service /etc/systemd/system/ cd path/to/application/build/server chmod +x server.js -# Start the service +# Start the service sudo service apollon_standalone start # Status of the service @@ -166,6 +169,7 @@ service apollon_standalone status ``` Error codes on server start: + - (code=exited, status=217/USER) -> apollon_standalone user does not exist - (code=exited, status=203/USER) -> script not executable @@ -201,7 +205,7 @@ git clone https://github.com/ls1intum/Apollon_standalone # build docker container docker build -t apollon_standalone . -run docker container +run docker container docker run -d --name apollon_standalone -p 8080:8080 apollon_standalone # build the web application and the application server @@ -225,7 +229,6 @@ To use Redis, set the environment variable `APOLLON_REDIS_URL` to the URL of the > [!IMPORTANT] > Apollon Standalone requires the Redis JSON module to be enabled. [Read the documents](https://redis.io/docs/latest/develop/data-types/json/) to learn how to enable the JSON module. - ```bash APOLLON_REDIS_URL=redis://[[username]:[password]@][host][:port] ``` @@ -267,7 +270,7 @@ Add a `.env` file in the root folder of the code. Add the following variables: ```toml # The URL of the server, e.g. the address at which # Apollon Standalone would be accessible after deployment. -DEPLOYMENT_URL=https://my.server/apollon/ +DEPLOYMENT_URL=https://my.server/apollon # The duration for which shared diagrams will be stored # (they will be removed afterwards) @@ -313,15 +316,15 @@ npm run update While developing the Standalone project, it is often required to make changes in the Apollon project. This can be achieved by executing the following workflow. -1. In the *Apollon* project: Generate a symlink by executing `npm link` command. -2. In the *Standalone* project: Link the generated symlink of Apollon *(from step 1)* by executing `npm link "@ls1intum/apollon"` command. +1. In the _Apollon_ project: Generate a symlink by executing `npm link` command. +2. In the _Standalone_ project: Link the generated symlink of Apollon _(from step 1)_ by executing `npm link "@ls1intum/apollon"` command. For more information please refer to the [documentation](https://docs.npmjs.com/cli/v9/commands/npm-link) of npm. -> ***Note***: While making changes in the *Apollon* project, for the changes to get reflected in *Standalone*, execute the following workflow: +> **_Note_**: While making changes in the _Apollon_ project, for the changes to get reflected in _Standalone_, execute the following workflow: > -> - Recompile the Apollon project by executing `npm run prepare` -> - Rebuild the Standalone project by executing `npm run build` +> - Recompile the Apollon project by executing `npm run prepare` +> - Rebuild the Standalone project by executing `npm run build` ### Using Redis in Development From d90fa08858a24751f40b2e0d6a2807f819672910 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 14:54:05 +0100 Subject: [PATCH 18/33] modify workflow --- .github/workflows/build-and-push-docker.yml | 75 --------------------- .github/workflows/build-and-push.yml | 12 ++++ 2 files changed, 12 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/build-and-push-docker.yml create mode 100644 .github/workflows/build-and-push.yml diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml deleted file mode 100644 index 67325cb7..00000000 --- a/.github/workflows/build-and-push-docker.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build Docker Image - -on: - workflow_call: - outputs: - apollon_standalone_image_tag: - description: "The tag of the apollon standalone image that was built" - value: ${{ jobs.build.outputs.apollon_standalone_image_tag }} - -jobs: - build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - dockerfile: ./Dockerfile.redis - image: ghcr.io/ls1intum/Apollon_standalone/apollon-standalone - context: . - path: apollon-standalone - outputs: - apollon_standalone_image_tag: "${{ steps.output-tag-apollon-standalone.outputs.apollon_standalone_image_tag }}" - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v44 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: all - - - name: Install Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ matrix.image }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch - type=ref,event=pr - - - name: Build and push Docker Image - uses: docker/build-push-action@v5 - with: - context: ${{ matrix.context }} - file: ${{ matrix.dockerfile }} - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - - - id: output-tag-apollon-standalone - run: | - if [[ "${{ matrix.path }}" == "apollon-standalone" ]] && [[ "${{ steps.changed-files.outputs.any_changed }}" == "true" ]]; then - echo "apollon_standalone_image_tag=${{ steps.meta.outputs.version }}" >> "$GITHUB_OUTPUT" - elif [[ "${{ matrix.path }}" == "server" ]]; then - echo "apollon_standalone_image_tag=latest" >> "$GITHUB_OUTPUT" - fi \ No newline at end of file diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 00000000..71eb101a --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,12 @@ +name: Build Docker Image + +on: + push: + +jobs: + build-and-push-workflow: + uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main + with: + image-name: apollon-standalone + docker-file: Dockerfile.redis + secrets: inherit \ No newline at end of file From 81b9ee269285cc79f2395342de4a42c2230675c3 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 19:58:28 +0100 Subject: [PATCH 19/33] fix image name --- .github/workflows/build-and-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 71eb101a..32fe5983 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -7,6 +7,6 @@ jobs: build-and-push-workflow: uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main with: - image-name: apollon-standalone + image-name: Apollon_standalone/apollon-standalone docker-file: Dockerfile.redis secrets: inherit \ No newline at end of file From 63b5930726898e73ef3c2cdf92317fe61ee36d8d Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 20:32:34 +0100 Subject: [PATCH 20/33] yolo --- .github/workflows/build-and-push-tmp.yml | 92 ++++++++++++++++++++++++ .github/workflows/build-and-push.yml | 12 +++- .github/workflows/deploy-docker.yml | 9 ++- .github/workflows/dev.yml | 5 +- docker-compose.prod.yml | 2 +- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/build-and-push-tmp.yml diff --git a/.github/workflows/build-and-push-tmp.yml b/.github/workflows/build-and-push-tmp.yml new file mode 100644 index 00000000..55d32ea9 --- /dev/null +++ b/.github/workflows/build-and-push-tmp.yml @@ -0,0 +1,92 @@ +# Move to ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main in the future +name: Build and Push Docker Image + +on: + workflow_call: + inputs: + image-name: + type: string + default: ${{ github.repository }} + description: "The name for the docker image (Default: Repository name)" + docker-file: + type: string + default: Dockerfile + description: "The path to the Dockerfile (Default: ./Dockerfile)" + docker-context: + type: string + default: . + description: "The context for the Docker build (Default: .)" + build-args: + type: string + description: "List of additional build contexts (e.g., name=path)" + required: false + platforms: + type: string + description: "List of platforms for which to build the image" + default: linux/amd64,linux/arm64 + registry: + type: string + default: ghcr.io + description: "The registry to push the image to (Default: ghcr.io)" + + secrets: + registry-user: + required: false + registry-password: + required: false + + outputs: + image-tag: + description: "The tag of the pushed image" + value: ${{ jobs.build.outputs.image-tag }} +jobs: + build: + name: Build Docker Image for ${{ inputs.image-name }} + runs-on: ubuntu-latest + outputs: + image-tag: ${{ steps.set-tag.outputs.image-tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Install Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ secrets.registry-user || github.actor }} + password: ${{ secrets.registry-password || secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ inputs.image-name }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + + - name: Set image tag output + id: set-tag + run: echo "::set-output name=image-tag::${{ steps.meta.outputs.version }}" + + - name: Build and push Docker Image + uses: docker/build-push-action@v6 + with: + context: ${{ inputs.docker-context }} + file: ${{ inputs.docker-file }} + platforms: ${{ inputs.platforms }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ inputs.build-args }} + push: true \ No newline at end of file diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 32fe5983..2c840505 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,12 +1,18 @@ name: Build Docker Image on: - push: + workflow_call: + outputs: + image-tag: + description: "The tag of the pushed image" + value: ${{ jobs.build-and-push-workflow.outputs.image-tag }} jobs: build-and-push-workflow: - uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main + name: Build and Push Docker Image + # uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main + uses: ./.github/workflows/build-and-push-tmp.yml with: - image-name: Apollon_standalone/apollon-standalone + image-name: ls1intum/Apollon_standalone docker-file: Dockerfile.redis secrets: inherit \ No newline at end of file diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index d3a917c5..bfab1d37 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -6,9 +6,14 @@ on: environment: required: true type: string - apollon_standalone_image_tag: + image-name: + type: string + default: ${{ github.repository }} + description: "The name for the docker image (Default: Repository name)" + image-tag: default: "latest" type: string + description: "The tag for the docker image (Default: latest)" jobs: deploy: @@ -64,7 +69,7 @@ jobs: echo "DEPLOYMENT_URL=${{ vars.DEPLOYMENT_URL }}" > .env.prod echo "APOLLON_REDIS_DIAGRAM_TTL=${{ vars.APOLLON_REDIS_DIAGRAM_TTL }}" >> .env.prod - echo "APOLLON_STANDALONE_IMAGE_TAG=${{ inputs.apollon_standalone_image_tag }}" >> .env.prod + echo "IMAGE_TAG=${{ inputs.image-tag }}" >> .env.prod - name: SSH to VM and Execute Docker-Compose Up uses: appleboy/ssh-action@v1.0.3 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index b51025e4..f1ed40e6 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -5,7 +5,7 @@ on: jobs: build-dev-container: - uses: ./.github/workflows/build-and-push-docker.yml + uses: ./.github/workflows/build-and-push.yml secrets: inherit deploy-dev-container: needs: build-dev-container @@ -13,4 +13,5 @@ jobs: secrets: inherit with: environment: Dev - apollon_standalone_image_tag: "${{ needs.build-dev-container.outputs.apollon_standalone_image_tag }}" \ No newline at end of file + image-name: ls1intum/Apollon_standalone + image-tag: "${{ needs.build-dev-container.outputs.image-tag }}" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 060e2e3a..f35a8bfa 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,7 +16,7 @@ services: - apollon-network apollon_standalone: - image: "ghcr.io/ls1intum/Apollon_standalone/apollon-standalone:${APOLLON_STANDALONE_IMAGE_TAG}" + image: "ghcr.io/ls1intum/Apollon_standalone/apollon-standalone:${IMAGE_TAG}" container_name: apollon-standalone environment: - APOLLON_REDIS_URL=redis://apollon_redis:6379 From f28b3ad78a52d871a35cfdc2f5c91698422e5276 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 21:07:38 +0100 Subject: [PATCH 21/33] fix error --- .github/workflows/deploy-docker.yml | 2 +- .github/workflows/dev.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index bfab1d37..bd3b57eb 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -33,7 +33,7 @@ jobs: proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | - docker compose -f docker-compose.prod.yml --env-file=.env.prod down --remove-orphans --rmi all + docker compose -f docker-compose.prod.yml down --remove-orphans --rmi all - name: checkout uses: actions/checkout@v4 diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f1ed40e6..1e003096 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -2,6 +2,7 @@ name: Build and Deploy to Dev on: pull_request: + branches: [main] jobs: build-dev-container: From b7be687592e87e770f2b2cb53049af8d957ea3ce Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 21:27:44 +0100 Subject: [PATCH 22/33] remove needed file --- .github/workflows/deploy-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index bd3b57eb..dd498555 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -33,7 +33,7 @@ jobs: proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | - docker compose -f docker-compose.prod.yml down --remove-orphans --rmi all + docker compose down --remove-orphans --rmi all - name: checkout uses: actions/checkout@v4 From 4aa9d1e5ef54e091b446d78298f65113ee9c2f80 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 21:48:41 +0100 Subject: [PATCH 23/33] update --- .github/workflows/build-and-push-tmp.yml | 4 +++- .github/workflows/deploy-docker.yml | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-push-tmp.yml b/.github/workflows/build-and-push-tmp.yml index 55d32ea9..830d794b 100644 --- a/.github/workflows/build-and-push-tmp.yml +++ b/.github/workflows/build-and-push-tmp.yml @@ -89,4 +89,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: ${{ inputs.build-args }} - push: true \ No newline at end of file + push: true + cache-from: type=gha,scope=${{ inputs.image-name }} + cache-to: type=gha,scope=${{ inputs.image-name }},mode=max diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index dd498555..ec9ca1ab 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -22,7 +22,7 @@ jobs: name: ${{ inputs.environment }} url: 'https://${{ vars.SERVER_HOST }}' steps: - - name: SSH to VM and Execute Docker-Compose Down + - name: SSH to VM and Execute Docker-Compose Down (if exists) uses: appleboy/ssh-action@v1.0.3 with: host: ${{ vars.VM_HOST }} @@ -33,7 +33,25 @@ jobs: proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }} proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }} script: | - docker compose down --remove-orphans --rmi all + #!/bin/bash + set -e # Exit immediately if a command exits with a non-zero status + + COMPOSE_FILE="docker-compose.prod.yml" + ENV_FILE=".env.prod" + + # Check if docker-compose.prod.yml exists + if [ -f "$COMPOSE_FILE" ]; then + echo "$COMPOSE_FILE found." + + # Check if .env.prod exists + if [ -f "$ENV_FILE" ]; then + docker compose -f "$COMPOSE_FILE" --env-file="$ENV_FILE" down --remove-orphans --rmi all + else + docker compose -f "$COMPOSE_FILE" down --remove-orphans --rmi all + fi + else + echo "$COMPOSE_FILE does not exist. Skipping docker compose down." + fi - name: checkout uses: actions/checkout@v4 From a6887d76c8a146fe89afd575159642068b78fe59 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 22:07:40 +0100 Subject: [PATCH 24/33] fix compose file --- docker-compose.prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f35a8bfa..62b84238 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -15,8 +15,8 @@ services: networks: - apollon-network - apollon_standalone: - image: "ghcr.io/ls1intum/Apollon_standalone/apollon-standalone:${IMAGE_TAG}" + apollon-standalone: + image: "ghcr.io/ls1intum/apollon_standalone:${IMAGE_TAG}" container_name: apollon-standalone environment: - APOLLON_REDIS_URL=redis://apollon_redis:6379 From f6757fd9b8e6d14a602a5a4ff2b82841890983b0 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 22:31:00 +0100 Subject: [PATCH 25/33] fix casing --- .github/workflows/build-and-push.yml | 2 +- .github/workflows/dev.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 2c840505..f454f3f6 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -13,6 +13,6 @@ jobs: # uses: ls1intum/.github/.github/workflows/build-and-push-docker-image.yml@main uses: ./.github/workflows/build-and-push-tmp.yml with: - image-name: ls1intum/Apollon_standalone + image-name: ls1intum/apollon_standalone docker-file: Dockerfile.redis secrets: inherit \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1e003096..734ac014 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -14,5 +14,5 @@ jobs: secrets: inherit with: environment: Dev - image-name: ls1intum/Apollon_standalone + image-name: ls1intum/apollon_standalone image-tag: "${{ needs.build-dev-container.outputs.image-tag }}" \ No newline at end of file From 1f70b29bcdca405508e3814429f96444ac1ed8b6 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 22:48:45 +0100 Subject: [PATCH 26/33] remove cache --- .github/workflows/build-and-push-tmp.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build-and-push-tmp.yml b/.github/workflows/build-and-push-tmp.yml index 830d794b..55d32ea9 100644 --- a/.github/workflows/build-and-push-tmp.yml +++ b/.github/workflows/build-and-push-tmp.yml @@ -89,6 +89,4 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: ${{ inputs.build-args }} - push: true - cache-from: type=gha,scope=${{ inputs.image-name }} - cache-to: type=gha,scope=${{ inputs.image-name }},mode=max + push: true \ No newline at end of file From a7ef550b1d0899bec675fd20c6f52d98363bd173 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 23:18:50 +0100 Subject: [PATCH 27/33] map to host --- docker-compose.prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 62b84238..b9cd0452 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -23,8 +23,8 @@ services: - APOLLON_REDIS_DIAGRAM_TTL=${APOLLON_REDIS_DIAGRAM_TTL} - DEPLOYMENT_URL=${DEPLOYMENT_URL} restart: unless-stopped - expose: - - "8080" + ports: + - "8080:8080" depends_on: redis: condition: service_healthy From a633da3fade2c38f2676e23cf4c916b90efeb9b0 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 2 Dec 2024 23:51:49 +0100 Subject: [PATCH 28/33] fix redis --- docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b9cd0452..54ac0cba 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -19,7 +19,7 @@ services: image: "ghcr.io/ls1intum/apollon_standalone:${IMAGE_TAG}" container_name: apollon-standalone environment: - - APOLLON_REDIS_URL=redis://apollon_redis:6379 + - APOLLON_REDIS_URL=redis://apollon-redis:6379 - APOLLON_REDIS_DIAGRAM_TTL=${APOLLON_REDIS_DIAGRAM_TTL} - DEPLOYMENT_URL=${DEPLOYMENT_URL} restart: unless-stopped From 6d914c4090212e939d5e3151f3acee835774b1d9 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Tue, 3 Dec 2024 01:00:46 +0100 Subject: [PATCH 29/33] remove arg --- Dockerfile.redis | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Dockerfile.redis b/Dockerfile.redis index e3b0bd49..bad7eb50 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -7,14 +7,9 @@ RUN apk add --no-cache \ pango-dev \ giflib-dev - -ARG DEPLOYMENT_URL="http://localhost:8080" -ARG APOLLON_REDIS_URL="redis://localhost:6379" -ARG APOLLON_REDIS_DIAGRAM_TTL="30d" - -ENV DEPLOYMENT_URL=${DEPLOYMENT_URL} -ENV APOLLON_REDIS_URL=${APOLLON_REDIS_URL} -ENV APOLLON_REDIS_DIAGRAM_TTL=${APOLLON_REDIS_DIAGRAM_TTL} +ENV DEPLOYMENT_URL="http://localhost:8080" +ENV APOLLON_REDIS_URL="redis://localhost:6379" +ENV APOLLON_REDIS_DIAGRAM_TTL="30d" WORKDIR /app From afac92f54e448c1645fa0e4f80e7bd047c9ecffc Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Tue, 3 Dec 2024 01:13:03 +0100 Subject: [PATCH 30/33] fix deployment url --- Dockerfile.redis | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.redis b/Dockerfile.redis index bad7eb50..4685edcd 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -21,4 +21,5 @@ WORKDIR /app/build/server EXPOSE 8080 +ENTRYPOINT [ "sed", "-i", "s|http://localhost:8080|${DEPLOYMENT_URL}|g", "/app/build/webapp/*.js" ] CMD [ "node", "bundle.js" ] From f1da6b56b17c780e7ad55e5c26c12fe364383cff Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Tue, 3 Dec 2024 01:39:00 +0100 Subject: [PATCH 31/33] fix sed --- Dockerfile.redis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.redis b/Dockerfile.redis index 4685edcd..02c1d5d3 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -21,5 +21,5 @@ WORKDIR /app/build/server EXPOSE 8080 -ENTRYPOINT [ "sed", "-i", "s|http://localhost:8080|${DEPLOYMENT_URL}|g", "/app/build/webapp/*.js" ] +ENTRYPOINT [ "sed", "-i", "s|http://localhost:8080|${DEPLOYMENT_URL}|g", "../webapp/*.js" ] CMD [ "node", "bundle.js" ] From e82c13ca5904b98b48fba40db7242fe9149f7117 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Tue, 3 Dec 2024 02:18:17 +0100 Subject: [PATCH 32/33] fix url --- Dockerfile.redis | 1 - packages/server/src/main/server.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Dockerfile.redis b/Dockerfile.redis index 02c1d5d3..bad7eb50 100644 --- a/Dockerfile.redis +++ b/Dockerfile.redis @@ -21,5 +21,4 @@ WORKDIR /app/build/server EXPOSE 8080 -ENTRYPOINT [ "sed", "-i", "s|http://localhost:8080|${DEPLOYMENT_URL}|g", "../webapp/*.js" ] CMD [ "node", "bundle.js" ] diff --git a/packages/server/src/main/server.ts b/packages/server/src/main/server.ts index 21143e1c..281f32d2 100644 --- a/packages/server/src/main/server.ts +++ b/packages/server/src/main/server.ts @@ -1,3 +1,5 @@ +import fs from 'fs'; +import path from 'path'; import bodyParser from 'body-parser'; import express, { RequestHandler } from 'express'; import * as Sentry from '@sentry/node'; @@ -19,6 +21,15 @@ if (process.env.SENTRY_DSN) { Sentry.setTag('package', 'server'); } +// Replace http://localhost:8080 with the actual process.env.DEPLOYMENT_URL +const jsFiles = fs.readdirSync(webappPath).filter((file) => file.endsWith('.js')); +jsFiles.forEach((file) => { + const filePath = path.join(webappPath, file); + const content = fs.readFileSync(filePath, 'utf8') + .replace(/http:\/\/localhost:8080/g, process.env.DEPLOYMENT_URL || 'http://localhost:8080'); + fs.writeFileSync(filePath, content); +}); + app.use('/', express.static(webappPath)); app.use(bodyParser.json() as RequestHandler); app.use( From 33cce302894558f75e7e5c97d047c69a747f165f Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Tue, 3 Dec 2024 08:34:07 +0100 Subject: [PATCH 33/33] Trigger Build