├── ui ├── .env ├── .browserslistrc ├── src │ ├── vite-env.d.ts │ ├── configurationEditor │ │ ├── ConfigurationEditorService.ts │ │ ├── NewConfigurationFile.tsx │ │ └── ConfigurationEditor.tsx │ ├── templateStore │ │ └── TemplateStore.tsx │ ├── instances │ │ ├── Instance.css │ │ ├── InstancesService.tsx │ │ └── NginxInstances.tsx │ ├── App.tsx │ ├── configuration │ │ ├── ConfigurationTemplates.ts │ │ ├── ConfigurationCreator.ts │ │ ├── README.md │ │ ├── ConfigurationReference.ts │ │ └── ConfigurationParser.ts │ ├── network │ │ └── NetworkService.ts │ ├── main.tsx │ ├── configurationUI │ │ ├── Location.tsx │ │ ├── ConfigurationUiService.ts │ │ ├── Server.tsx │ │ └── ConfigurationUi.tsx │ └── prism │ │ ├── Editor.tsx │ │ ├── prism-nginx.js │ │ └── prism-nginx.css ├── tsconfig.node.json ├── vite.config.ts ├── index.html ├── tsconfig.json └── package.json ├── .dockerignore ├── .gitignore ├── docs └── NGINX-dd-extension.png ├── metadata.json ├── logo.svg ├── Makefile ├── Dockerfile ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── LICENSE /ui/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ui/node_modules -------------------------------------------------------------------------------- /ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | Electron 17.1.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ui/build 3 | .idea 4 | 5 | -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/NGINX-dd-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objects/docker-extension/main/docs/NGINX-dd-extension.png -------------------------------------------------------------------------------- /ui/src/configurationEditor/ConfigurationEditorService.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export class ConfigurationEditorService { 5 | constructor() { 6 | } 7 | 8 | } -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "logo.svg", 3 | "ui": { 4 | "dashboard-tab": { 5 | "title": "NGINX", 6 | "src": "index.html", 7 | "root": "ui" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /ui/src/templateStore/TemplateStore.tsx: -------------------------------------------------------------------------------- 1 | interface StoreProps { 2 | 3 | } 4 | 5 | export function TemplateStore(props: StoreProps) { 6 | 7 | return(<>Coming Soon) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/instances/Instance.css: -------------------------------------------------------------------------------- 1 | .ngx-instance:hover { 2 | box-shadow: 0px 3px 5px -1px rgb(0 60 60 / 20%), 0px 5px 8px 0px rgb(0 60 60 / 14%), 0px 1px 14px 0px rgb(0 60 60 / 12%); 3 | cursor: pointer; 4 | } 5 | 6 | .ngx-back-button:hover { 7 | color: green; 8 | } -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@mui/material'; 3 | import {NginxInstance} from "./instances/NginxInstances"; 4 | 5 | export function App() { 6 | 7 | return ( 8 | <> 9 | NGINX Development Center 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: "./", 8 | build: { 9 | outDir: "build", 10 | }, 11 | server: { 12 | port: 3000, 13 | strictPort: true, 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/configuration/ConfigurationTemplates.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export class ConfigurationTemplates { 5 | 6 | public static simpleProxyServerTemplate = () => { 7 | return ` 8 | server { 9 | listen $$LISTEN; 10 | server_name $$SERVER_NAME; 11 | 12 | location / { 13 | proxy_pass $$UPSTREAM; 14 | proxy_set_header Host $host; 15 | } 16 | } 17 | ` 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/configuration/ConfigurationCreator.ts: -------------------------------------------------------------------------------- 1 | import {ConfigurationTemplates} from "./ConfigurationTemplates"; 2 | 3 | 4 | export class ConfigurationCreator { 5 | 6 | public simpleProxyConfiguration(serverName: string, port: string, upstream: string) { 7 | let configurationString = ConfigurationTemplates.simpleProxyServerTemplate() 8 | configurationString = configurationString.replace(/\$\$LISTEN/g, port) 9 | configurationString = configurationString.replace(/\$\$SERVER_NAME/g, serverName) 10 | configurationString = configurationString.replace(/\$\$UPSTREAM/g, `http://${upstream}/`) 11 | return configurationString 12 | } 13 | } -------------------------------------------------------------------------------- /ui/src/network/NetworkService.ts: -------------------------------------------------------------------------------- 1 | import {DockerDesktopClient} from "@docker/extension-api-client-types/dist/v1"; 2 | import {createDockerDesktopClient} from "@docker/extension-api-client"; 3 | 4 | 5 | export class NetworkService { 6 | 7 | private ddClient: DockerDesktopClient 8 | 9 | constructor() { 10 | this.ddClient = createDockerDesktopClient(); 11 | } 12 | 13 | // Network-Overview. 14 | // Creates an array of the docker networks and the containers attached to them 15 | // Returns a List of Containers and Exposed Ports internally as well as globally if defined. 16 | async containersInNetwork(network: string): Promise { 17 | return await this.ddClient.docker.listContainers({"filters": JSON.stringify({network: [network]})}); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/configuration/README.md: -------------------------------------------------------------------------------- 1 | ## NGINX TypeScript configuration parser 2 | 3 | Parse the configuration and display help messages from nginx.org 4 | 5 | 6 | Parse the configuration file by newline. 7 | 8 | Create an array of configuration lines. Do NOT alter / change the main configuration file. 9 | Do not remove Line-Breaks or spaces / tabs. This will destroy the formatting in the NGINX config file. 10 | 11 | Parser can be copied form the `nginxinfo-tool`. To parse the overall NGINX configuration use the output of `nginx -T` 12 | The manual include resolver is not needed in this case. 13 | 14 | Load the list of directives and links to the documentation in the Extension on build time. 15 | This will insure a fast and seamless experience to look them up for the user (even in case they will be offline). 16 | 17 | ```typescript 18 | interface ConfigurationDirective { 19 | name: string, 20 | link: string, 21 | helpMessage: string 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import CssBaseline from "@mui/material/CssBaseline"; 4 | import {DockerMuiThemeProvider} from "@docker/docker-mui-theme"; 5 | 6 | import {App} from './App'; 7 | import {ThemeProvider, useTheme} from "@mui/material"; 8 | 9 | 10 | const CustomThemeProvider = ({children}: any) => { 11 | const theme = useTheme(); 12 | 13 | // @ts-ignore 14 | theme.components = { 15 | ...theme.components, 16 | MuiChip: { 17 | styleOverrides: { 18 | outlined: { 19 | textTransform: "inherit" 20 | }, 21 | }, 22 | } 23 | }; 24 | return {children}; 25 | }; 26 | 27 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@docker/docker-mui-theme": "<0.1.0", 8 | "@docker/extension-api-client": "0.3.3", 9 | "@emotion/react": "^11.10.4", 10 | "@emotion/styled": "^11.10.4", 11 | "@mui/icons-material": "^5.11.9", 12 | "@mui/lab": "^5.0.0-alpha.120", 13 | "@mui/material": "^5.11.10", 14 | "@types/prismjs": "^1.26.0", 15 | "js-base64": "^3.7.5", 16 | "prism-react-renderer": "^1.3.5", 17 | "prismjs": "^1.29.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-simple-code-editor": "^0.13.1" 21 | }, 22 | "scripts": { 23 | "dev": "vite", 24 | "build": "tsc && vite build", 25 | "test": "jest src" 26 | }, 27 | "devDependencies": { 28 | "@docker/extension-api-client-types": "0.3.3", 29 | "@types/jest": "^29.1.2", 30 | "@types/node": "^18.7.18", 31 | "@types/react": "^18.0.17", 32 | "@types/react-dom": "^18.0.6", 33 | "@vitejs/plugin-react": "^2.1.0", 34 | "jest": "^29.1.2", 35 | "typescript": "^4.8.3", 36 | "vite": "^3.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE?=nginx/docker-extension 2 | TAG?=latest 3 | 4 | BUILDER=buildx-multi-arch 5 | 6 | INFO_COLOR = \033[0;36m 7 | NO_COLOR = \033[m 8 | 9 | build-extension: ## Build service image to be deployed as a desktop extension 10 | docker build --tag=$(IMAGE):$(TAG) . 11 | 12 | remove-extension: 13 | docker extension remove $(IMAGE):$(TAG) 14 | 15 | install-extension: build-extension ## Install the extension 16 | docker extension install $(IMAGE):$(TAG) 17 | 18 | update-extension: build-extension ## Update the extension 19 | docker extension update $(IMAGE):$(TAG) 20 | 21 | prepare-buildx: ## Create buildx builder for multi-arch build, if not exists 22 | docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host 23 | 24 | push-extension: prepare-buildx ## Build & Upload extension image to hub. Do not push if tag already exists: make push-extension tag=0.1 25 | docker pull $(IMAGE):$(TAG) && echo "Failure: Tag already exists" || docker buildx build --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) . 26 | 27 | devel: ## Start Docker Extension in Dev mode 28 | docker extension dev debug $(IMAGE):$(TAG) && docker extension dev ui-source $(IMAGE):$(TAG) http://localhost:3000 29 | 30 | help: ## Show this help 31 | @echo Please specify a build target. The choices are: 32 | @grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "$(INFO_COLOR)%-30s$(NO_COLOR) %s\n", $$1, $$2}' 33 | 34 | .PHONY: help 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM node:18.12-alpine3.16 AS client-builder 2 | WORKDIR /ui 3 | # cache packages in layer 4 | COPY ui/package.json /ui/package.json 5 | COPY ui/package-lock.json /ui/package-lock.json 6 | RUN --mount=type=cache,target=/usr/src/app/.npm \ 7 | npm set cache /usr/src/app/.npm && \ 8 | npm ci 9 | # install 10 | COPY ui /ui 11 | RUN npm run build 12 | 13 | FROM alpine 14 | LABEL org.opencontainers.image.title="NGINX Development Center" \ 15 | org.opencontainers.image.description="NGINX Development Center for Docker Desktop" \ 16 | org.opencontainers.image.vendor="NGINX Inc." \ 17 | com.docker.desktop.extension.api.version="0.3.3" \ 18 | com.docker.extension.screenshots='[{"alt":"NGINX Docker Development Center", "url":"https://raw.githubusercontent.com/nginx/docker-extension/main/docs/NGINX-dd-extension.png"}]' \ 19 | com.docker.desktop.extension.icon="https://raw.githubusercontent.com/nginx/docker-extension/main/logo.svg"\ 20 | com.docker.extension.detailed-description="With the NGINX Docker Development Center you are able to configure your running NGINX Docker Instances." \ 21 | com.docker.extension.publisher-url="https://github.com/nginx/docker-extension/" \ 22 | com.docker.extension.additional-urls='[{"title":"Support", "url":"https://github.com/nginx/docker-extension/issues"}]' \ 23 | com.docker.extension.categories="utility-tools" \ 24 | com.docker.extension.changelog="Bugfix: Fixed several dark mode rendering issues" 25 | 26 | COPY metadata.json . 27 | COPY logo.svg . 28 | COPY --from=client-builder /ui/build ui -------------------------------------------------------------------------------- /ui/src/configurationUI/Location.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from "react"; 2 | import { 3 | Alert, 4 | Box, 5 | Button, 6 | Grid, 7 | Typography, 8 | ThemeProvider, 9 | createTheme, 10 | } from "@mui/material"; 11 | import PublishIcon from "@mui/icons-material/Publish"; 12 | 13 | const listTheme = createTheme({ 14 | typography: { 15 | h3: { 16 | fontSize: 30, 17 | }, 18 | subtitle2: { 19 | fontSize: 13, 20 | opacity: .6, 21 | overflow: 'hidden', 22 | }, 23 | body1: { 24 | fontWeight: 400, 25 | }, 26 | body2: { 27 | fontWeight: 600, 28 | }, 29 | }, 30 | }); 31 | 32 | interface LocationProps { 33 | 34 | } 35 | 36 | export function Location(props: LocationProps) { 37 | 38 | useEffect(() => { 39 | 40 | }) 41 | 42 | return( 43 | 44 | 45 | Add a Location to Server 46 | 47 | Some information 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | ) 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /ui/src/configuration/ConfigurationReference.ts: -------------------------------------------------------------------------------- 1 | interface Context { 2 | name: string 3 | } 4 | 5 | interface Directive { 6 | name: string, 7 | information: string, 8 | context: Array, 9 | example: string 10 | } 11 | 12 | interface DirectivesList { 13 | directive: Directive 14 | } 15 | 16 | 17 | export class ConfigurationReference { 18 | 19 | private directives: any = new Map([ 20 | ["root", {name: "root", information: "root directive", context: []}], 21 | ["server", { 22 | name: "server", 23 | information: "Sets configuration for a virtual server. There is no clear separation between IP-based (based on the IP address) and name-based (based on the “Host” request header field) virtual servers", 24 | context: ["http"], 25 | syntax: "server { ... }" 26 | }], 27 | ["location", {name: "location", information: "location directive", context: []}], 28 | ["keepalive_timeout", { 29 | name: "keepalive_timeout", 30 | information: "The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections. The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ. ", 31 | context: ["http", "server", "location"], 32 | syntax: "keepalive_timeout timeout [header_timeout];" 33 | }], 34 | ]) 35 | 36 | constructor() { 37 | 38 | } 39 | 40 | getDirectiveInformation(directive: string): any { 41 | return this.directives.get(directive) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /ui/src/prism/Editor.tsx: -------------------------------------------------------------------------------- 1 | import {highlight, languages} from "prismjs"; 2 | import React, {useState} from "react"; 3 | import ReactEditor from "react-simple-code-editor"; 4 | import "./prism-nginx.css"; 5 | import "./prism-nginx.js"; 6 | 7 | // This component makes use of Reacts Lifting State Up functionality. Read more about it 8 | // here: https://reactjs.org/docs/lifting-state-up.html 9 | 10 | interface EditorProps { 11 | setConfigurationFileContent: any 12 | fileContent: any 13 | style?: any | undefined 14 | } 15 | 16 | 17 | export function Editor(props: EditorProps) { 18 | const handleChange = (value: string) => { 19 | props.setConfigurationFileContent(value) 20 | } 21 | 22 | const placeholderText = "# ********************************************\n# NGINX Configuration Editor \n# Please write your configuration here\n# Feel free to remove this comment\n# ********************************************" 23 | 24 | return ( 25 | highlight(props.fileContent ? props.fileContent : placeholderText, languages.nginx, "bash") 31 | .split("\n") 32 | .map((line, i) => `${i + 1}${line}`) 33 | .join('\n') 34 | } 35 | padding={10} 36 | style={{ 37 | ...props.style, 38 | fontFamily: '"Fira code", "Fira Mono", monospace', 39 | fontSize: 14, 40 | whiteSpace: "pre", 41 | outline: 0, 42 | }} 43 | /> 44 | ) 45 | } -------------------------------------------------------------------------------- /ui/src/prism/prism-nginx.js: -------------------------------------------------------------------------------- 1 | (function (Prism) { 2 | 3 | var variable = /\$(?:\w[a-z\d]*(?:_[^\x00-\x1F\s"'\\()$]*)?|\{[^}\s"'\\]+\})/i; 4 | 5 | Prism.languages.nginx = { 6 | 'comment': { 7 | pattern: /(^|[\s{};])#.*/, 8 | lookbehind: true, 9 | greedy: true 10 | }, 11 | 'directive': { 12 | pattern: /(^|\s)\w(?:[^;{}"'\\\s]|\\.|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\s+(?:#.*(?!.)|(?![#\s])))*?(?=\s*[;{])/, 13 | lookbehind: true, 14 | greedy: true, 15 | inside: { 16 | 'string': { 17 | pattern: /((?:^|[^\\])(?:\\\\)*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/, 18 | lookbehind: true, 19 | greedy: true, 20 | inside: { 21 | 'escape': { 22 | pattern: /\\["'\\nrt]/, 23 | alias: 'entity' 24 | }, 25 | 'variable': variable 26 | } 27 | }, 28 | 'comment': { 29 | pattern: /(\s)#.*/, 30 | lookbehind: true, 31 | greedy: true 32 | }, 33 | 'keyword': { 34 | pattern: /^\S+/, 35 | greedy: true 36 | }, 37 | 38 | // other patterns 39 | 40 | 'boolean': { 41 | pattern: /(\s)(?:off|on)(?!\S)/, 42 | lookbehind: true 43 | }, 44 | 'number': { 45 | pattern: /(\s)\d+[a-z]*(?!\S)/i, 46 | lookbehind: true 47 | }, 48 | 'variable': variable 49 | } 50 | }, 51 | 'punctuation': /[{};]/ 52 | }; 53 | 54 | }(Prism)); -------------------------------------------------------------------------------- /ui/src/configurationUI/ConfigurationUiService.ts: -------------------------------------------------------------------------------- 1 | import {InstancesService} from "../instances/InstancesService"; 2 | import {ConfigurationParser} from "../configuration/ConfigurationParser"; 3 | import {ConfigurationCreator} from "../configuration/ConfigurationCreator"; 4 | import {Base64} from "js-base64"; 5 | 6 | 7 | export class ConfigurationUiService { 8 | 9 | private instanceService: InstancesService; 10 | private configurationParser: ConfigurationParser; 11 | private configurationCreator: ConfigurationCreator; 12 | 13 | constructor() { 14 | this.instanceService = new InstancesService(); 15 | this.configurationParser = new ConfigurationParser(); 16 | this.configurationCreator = new ConfigurationCreator(); 17 | 18 | } 19 | 20 | async getConfiguration(containerId: string) { 21 | const config = await this.instanceService.getInstanceConfiguration(containerId); 22 | return this.configurationParser.parse(config); 23 | } 24 | 25 | createNewServerConfiguration(serverConfiguration: any, containerId: any) { 26 | console.log(serverConfiguration) 27 | let configTemplate = this.configurationCreator.simpleProxyConfiguration(serverConfiguration.serverName, 28 | serverConfiguration.listeners, 29 | serverConfiguration.upstream); 30 | console.log(configTemplate) 31 | const content = Base64.encode(configTemplate) 32 | this.instanceService.sendConfigurationToFile(serverConfiguration.file, containerId, content).then((data: any) => { 33 | this.instanceService.reloadNGINX(containerId).then((data: any) => { 34 | this.instanceService.displaySuccessMessage("Configuration successfully updated!"); 35 | }).catch((reason: any) => { 36 | this.instanceService.displayErrorMessage(`Error while updating configuration: ${reason.stderr.split("\n")[1]}`); 37 | }) 38 | }) 39 | return configTemplate 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX Docker Desktop Extension 2 | 3 | ![NGINX Docker Extension Instance Screen](docs/NGINX-dd-extension.png) 4 | 5 | The NGINX Docker Desktop Extension can be used to manage the instance configuration of a running NGINX container. 6 | 7 | ## Development 8 | Before we can interactively develop the Extensions frontend, it must be installed first. 9 | 10 | To build the extension locally. 11 | ```shell 12 | docker build -t nginx/nginx-dd-extension . 13 | ``` 14 | To install the extension 15 | ```shell 16 | docker extension install nginx/nginx-dd-extension 17 | ``` 18 | 19 | To remove the extension 20 | ```shell 21 | docker remove nginx/nginx-dd-extension 22 | ``` 23 | ## Release 24 | 25 | ```shell 26 | docker buildx build --push --no-cache --platform=linux/amd64,linux/arm64 -t nginx/nginx-docker-extension:0.0.1 . 27 | ``` 28 | 29 | ### Start Docker Extension Development Server 30 | 1. start the UI node server in the `ui` directory. Make sure you install the dev dependencies at the first. 31 | ```shell 32 | npm install 33 | npm run dev 34 | ``` 35 | 36 | 2. enable debugging for the NGINX Docker Extension. 37 | ```shell 38 | docker extension dev debug nginx/nginx-dd-extension 39 | ``` 40 | 41 | ```shell 42 | docker extension dev ui-source nginx/nginx-dd-extension http://localhost:3000 43 | ``` 44 | ## Community 45 | 46 | - The go-to place to start asking questions and share your thoughts is 47 | our [Slack channel](https://community.nginx.org/joinslack). 48 | 49 | - Get involved with the project by contributing! See the 50 | [contributing guide](CONTRIBUTING.md) for details. 51 | 52 | - For security issues, [email us](security-alert@nginx.org), mentioning 53 | NGINX Unit in the subject and following the [CVSS 54 | v3.1](https://www.first.org/cvss/v3.1/specification-document) spec. 55 | 56 | 57 | ## Backlog 58 | 59 | ### Re-Expose new Ports 60 | ```shell 61 | docker commit CONTAINERID NEWIMAGE 62 | docker run NEWIMAGE -p ... -p.... -v POSSIBLE MOUNTS 63 | ``` 64 | ### Export Configuration 65 | Export configuration files from inside the container to a projects directory on the local computer 66 | ```shell 67 | docker cp CONTAINERID:/etc/nginx/conf.d/test.conf ./something/.... 68 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | The following is a set of guidelines for contributing to NGINX Unit. We do 4 | appreciate that you are considering contributing! 5 | 6 | ## Table Of Contents 7 | 8 | - [Ask a Question](#ask-a-question) 9 | - [Contributing](#contributing) 10 | - [Git Style Guide](#git-style-guide) 11 | 12 | ## Ask a Question 13 | 14 | Please open an [issue](https://github.com/nginx/docker-extension/new) on GitHub with 15 | the label `question`. You can also ask a question on 16 | [Slack](https://nginxcommunity.slack.com). 17 | 18 | 19 | ## Contributing 20 | 21 | ### Report a Bug 22 | 23 | Ensure the bug was not already reported by searching on GitHub under 24 | [Issues](https://github.com/nginx/docker-extension/issues). 25 | 26 | If the bug is a potential security vulnerability, please report using our 27 | [security policy](https://unit.nginx.org/troubleshooting/#getting-support). 28 | 29 | To report a non-security bug, open an 30 | [issue](https://github.com/nginx/unit/issues/new) on GitHub with the label 31 | `bug`. Be sure to include a title and clear description, as much relevant 32 | information as possible, and a code sample or an executable test case showing 33 | the expected behavior that doesn't occur. 34 | 35 | 36 | ### Suggest an Enhancement 37 | 38 | To suggest an enhancement, open an 39 | [issue](https://github.com/nginx/docker-extension/issues/new) on GitHub with the label 40 | `enhancement`. Please do this before implementing a new feature to discuss the 41 | feature first. 42 | 43 | 44 | ### Open a Pull Request 45 | 46 | Fork the repo, create a branch, and submit a PR when your changes are tested and ready for review. 47 | Again, if you'd like to implement a new feature, please consider creating a feature request 48 | issue first to start a discussion about the feature. 49 | 50 | 51 | ## Git Style Guide 52 | 53 | - Keep a clean, concise and meaningful `git commit` history on your branch, 54 | rebasing locally and squashing before submitting a PR 55 | 56 | - In the subject line, use the past tense ("Added feature", not "Add feature"); 57 | also, use past tense to describe past scenarios, and present tense for 58 | current behavior 59 | 60 | - Limit the subject line to 67 characters, and the rest of the commit message 61 | to 80 characters 62 | 63 | - Reference issues and PRs liberally after the subject line; if the commit 64 | remedies a GitHub issue, [name 65 | it](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) 66 | accordingly 67 | 68 | - Don't rely on command-line commit messages with `-m`; use the editor instead -------------------------------------------------------------------------------- /ui/src/configurationEditor/NewConfigurationFile.tsx: -------------------------------------------------------------------------------- 1 | import {Alert, Box, Button, Grid, InputAdornment, TextField, Typography} from "@mui/material"; 2 | import React, {ChangeEvent, ChangeEventHandler, useState} from "react"; 3 | import {ConfigurationEditor} from "./ConfigurationEditor"; 4 | import {Editor} from "../prism/Editor"; 5 | import PublishIcon from "@mui/icons-material/Publish"; 6 | import {InstancesService} from "../instances/InstancesService"; 7 | import {Base64} from "js-base64"; 8 | 9 | interface NewConfigurationFileProps { 10 | nginxInstance: any 11 | instanceService: InstancesService 12 | 13 | } 14 | 15 | export function NewConfigurationFile(props: NewConfigurationFileProps) { 16 | const [configurationFileContent, setConfigurationFileContent] = useState(""); 17 | const [fileName, setFileName] = useState(""); 18 | 19 | const saveConfigurationToFile: any = () => { 20 | const content = Base64.encode(configurationFileContent) 21 | 22 | if (!fileName) { 23 | props.instanceService.displayErrorMessage(`Filename can not be empty!`); 24 | return; 25 | } 26 | // Make the path configurable 27 | props.instanceService.sendConfigurationToFile(fileName, props.nginxInstance.id, content).then((data: any) => { 28 | props.instanceService.reloadNGINX(props.nginxInstance.id).then((data: any) => { 29 | props.instanceService.displaySuccessMessage("Configuration successfully updated!"); 30 | }).catch((reason: any) => { 31 | props.instanceService.displayErrorMessage(`Error while updating configuration: ${reason.stderr.split("\n")[1]}`); 32 | }) 33 | }) 34 | } 35 | 36 | return ( 37 | 38 | Add a new configuration File 39 | 40 | Make sure the configuration filename ends with a .conf file extension and is a absolute path. 41 | Example: /etc/nginx/conf.d/example.conf 42 | 43 | { 46 | setFileName(e.target.value) 47 | }} 48 | /> 49 | 53 | 54 | 57 | 58 | 59 | ) 60 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the moderation team at nginx-oss-community@f5.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 71 | available at 72 | 73 | For answers to common questions about this code of conduct, see 74 | 75 | -------------------------------------------------------------------------------- /ui/src/instances/InstancesService.tsx: -------------------------------------------------------------------------------- 1 | import {createDockerDesktopClient} from "@docker/extension-api-client" 2 | import {ExecResult, DockerDesktopClient} from "@docker/extension-api-client-types/dist/v1"; 3 | 4 | 5 | /* 6 | * NGINX DD Instance Service 7 | * 8 | * - find containers that run NGINX and add them to a list of instances. 9 | * 10 | * 11 | * 12 | * */ 13 | 14 | interface Port { 15 | PrivatePort: number, 16 | Type: string 17 | } 18 | 19 | interface Networks { 20 | 21 | } 22 | 23 | interface NetworkSettings { 24 | Networks: Networks 25 | } 26 | 27 | interface Container { 28 | Id: string, 29 | Names: Array, 30 | Ports: Array, 31 | State: string, 32 | Status: string, 33 | NetworkSettings: any 34 | Mounts: any 35 | } 36 | 37 | /* 38 | * class InstanceServices 39 | * (C) F5 Inc. NGINX 40 | * (C) Timo Stark 41 | 42 | * These are the main functions to communicate with a NGINX in a Docker Container. 43 | * 44 | */ 45 | 46 | export class InstancesService { 47 | private ddClient: DockerDesktopClient 48 | 49 | constructor() { 50 | this.ddClient = createDockerDesktopClient(); 51 | } 52 | 53 | async getInstances(): Promise { 54 | //Cast the `unknown` Promise from the dd client to an actual array. 55 | //Create an interface object that can be returned by getInstances. 56 | //listContainers returns a Promises from Type Array 57 | 58 | const containers = await this.ddClient.docker.listContainers() as Array; 59 | //Loop over all containers and check if NGINX is installed. 60 | const promises = containers.map(container => { 61 | //Handling Promise in Map is tricky. So, using `Promise.all` seams the best solution here. 62 | //Other than that, the response from the docker exec command does not have any easy to parse 63 | //reference to the container anymore. So putting it in Context while creating the containers array 64 | //make sense. 65 | console.log(container) 66 | return { 67 | container: container.Id, 68 | name: container.Names[0], 69 | ports: container.Ports, 70 | status: container.Status, 71 | //Networks is not an array per default. Converting! 72 | networks: Object.entries(container.NetworkSettings.Networks), 73 | mounts: container.Mounts || [], 74 | promise: this.ddClient.docker.cli.exec( 75 | "exec", 76 | ["-i", `${container.Id}`, 77 | '/bin/sh -c "nginx -v"']) 78 | } 79 | }) 80 | // Filtering out all Containers that are not NGINX. 81 | await Promise.all(promises.map(item => { 82 | return item.promise.then(result => { 83 | if (!result.code) { 84 | return result 85 | } 86 | // Don't print error for rejected promises. 87 | }).catch((error) => '') 88 | })) 89 | return promises; 90 | } 91 | 92 | async getInstanceConfiguration(containerId: string): Promise { 93 | const data = await this.ddClient.docker.cli.exec( 94 | "exec", 95 | [containerId, 96 | "/bin/sh", "-c", `"nginx -T"`]); 97 | // parse Configuration File and return array. 98 | return data.stdout 99 | } 100 | 101 | async getConfigurations(containerId: string): Promise { 102 | const data = await this.ddClient.docker.cli.exec( 103 | "exec", 104 | [containerId, 105 | "/bin/sh", "-c", `"nginx -T"`]); 106 | // parse Configuration File and return array. 107 | return data.stdout.match(new RegExp('# configuration file(.*)', 'g')) 108 | } 109 | 110 | async getConfigurationFileContent(file: string, containerId: string): Promise { 111 | const fileContent = await this.ddClient.docker.cli.exec( 112 | "exec", 113 | [containerId, 114 | "/bin/sh", "-c", `"cat ${file}"`]); 115 | return fileContent; 116 | } 117 | 118 | // Maybe this can be the default function to send docker exec commands? 119 | async sendConfigurationToFile(file: string, containerId: string, configurationB64: string): Promise { 120 | return await this.ddClient.docker.cli.exec( 121 | "exec", 122 | [containerId, 123 | "/bin/sh", "-c", `"echo ${configurationB64} |base64 -d > ${file}"`]); 124 | } 125 | 126 | async reloadNGINX(containerId: string): Promise { 127 | return await this.ddClient.docker.cli.exec( 128 | "exec", 129 | [containerId, 130 | "/bin/sh", "-c", `"nginx -s reload"`]); 131 | } 132 | 133 | // Building the Containers Ecosystem 134 | // Finding the attached network(s) 135 | // Build a list of all containers with its attached networks 136 | 137 | 138 | displaySuccessMessage(message: string): void { 139 | this.ddClient.desktopUI.toast.success(message) 140 | } 141 | 142 | displayErrorMessage(message: string): void { 143 | this.ddClient.desktopUI.toast.error(message) 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /ui/src/configuration/ConfigurationParser.ts: -------------------------------------------------------------------------------- 1 | interface ServerConfiguration { 2 | names: Array, 3 | listeners: Array 4 | locations: Array, 5 | file: string 6 | } 7 | 8 | interface NginxConfiguration { 9 | nginx: any, 10 | http: any, 11 | stream: any 12 | } 13 | 14 | export class ConfigurationParser { 15 | 16 | constructor() { 17 | } 18 | 19 | parse(rawConfiguration: string): NginxConfiguration { 20 | let confArray = rawConfiguration.split('\n'); 21 | // remove all empty lines from array 22 | confArray = confArray.filter(n => n) 23 | // Remove all Empty files from Array to make the map more efficient. 24 | 25 | // logic to write 26 | // detect open context block if HTTP or stream. Set in HTTP or in Stream. If in HTTP look for server config context 27 | // server can be a one-liner as well. BUT that means that the LAST character of the line text is a `}` 28 | let inHttpContext = false; 29 | let inServerContext = false; 30 | let inLocationContext = false; 31 | // save file context for later editing 32 | let configurationFile = "" 33 | 34 | let currentServerConfiguration: ServerConfiguration = {names: [], listeners: [], locations: [], file: ""}; 35 | 36 | let locationConfiguration = {'location': undefined, 'configuration': []} 37 | // New JSON-based Configuration. 38 | // Each Object in HTTP is 39 | let configuration: NginxConfiguration = { 40 | nginx: {}, 41 | http: {'configuration': [], 'servers': []}, 42 | stream: {'configuration': [], 'servers': []} 43 | }; 44 | 45 | //save current array to push configuration to it. 46 | let index = -1; 47 | 48 | confArray.map(line => { 49 | // Detecting comments first and skip 50 | 51 | // We have to check for context - based directives first. These are 52 | // http, map, upstream, server, location, if. These are basically opening a new context. 53 | // If it is not a context directive we can treat is a directive with params. 54 | 55 | // remove all whitespaces. They will be re-implemented using the ident. 56 | line = line.trim(); 57 | // if line ends with `;` it is a value line. Check current context and proceed. 58 | if (line.substring(line.length - 1) === ';') { 59 | //remove `;` from line end. 60 | line = line.substring(0, line.length - 1) 61 | //split the configuration by SPACE. 62 | const config = line.split(' ').filter(n => n); 63 | //first will be directive, others values. 64 | const obj = {'directive': config[0], 'paramter': config[1]} 65 | 66 | // Get the context to know where to push the configuration to. 67 | if (inLocationContext) { 68 | currentServerConfiguration.locations[index].configuration.push(obj) 69 | } 70 | } 71 | 72 | // Comment line 73 | if (line.substring(0, 1) === '#') { 74 | // find Includes to get the filename. 75 | if (line.match('# configuration file')) { 76 | //get configuration file name. 77 | console.log(line) 78 | // configurationFile = line.match("[^\\/]*$") ? line.match("[^\\/]*$")![0] : "" 79 | configurationFile = line.match("\\/.*$") ? line.match("\\/.*$")![0] : "" 80 | configurationFile = configurationFile.replace(":", "") 81 | return 82 | } 83 | //Comment line - skip processing 84 | return 85 | } 86 | 87 | if (line.length === 1 && line.substring(line.length - 1) === '}') { 88 | if (inLocationContext) { 89 | inLocationContext = false 90 | //reset array index 91 | return 92 | } 93 | 94 | if (inServerContext) { 95 | inServerContext = false 96 | configuration.http.servers.push(currentServerConfiguration) 97 | index = -1; 98 | return 99 | } 100 | } 101 | // special one liner treatment! ::) 102 | if (line.length > 1 && line.substring(line.length - 1) === '}') { 103 | return 104 | } 105 | // Let's check for http-context. 106 | if (line.match('http') && line.substring(line.length - 1) === '{') { 107 | inHttpContext = true 108 | return 109 | } 110 | 111 | if (line.match('server') && line.substring(line.length - 1) === '{') { 112 | // Add new Server Object in Array. 113 | currentServerConfiguration = {'names': [], 'listeners': [], 'locations': [], 'file': configurationFile} 114 | inServerContext = true 115 | //? 116 | inLocationContext = false 117 | return 118 | } 119 | 120 | if (line.match('listen')) { 121 | // Listeners found: Configuration: 122 | currentServerConfiguration.listeners.push(line.split(' ').filter(n => n)[1]) 123 | 124 | } 125 | 126 | if (line.match('server_name')) { 127 | // Server Name found: Add to server names array: 128 | currentServerConfiguration.names.push(line.split(' ').filter(n => n)[1]) 129 | 130 | } 131 | 132 | if (line.match('location') && line.substring(line.length - 1) === '{') { 133 | 134 | inLocationContext = true; 135 | let location = line.split(' ').filter(n => n); 136 | currentServerConfiguration.locations.push({ 137 | 'location': `${location[location.length - 2]}`, 138 | 'configuration': [] 139 | }) 140 | //increment index. 141 | index += 1 142 | return 143 | } 144 | }) 145 | console.log(configuration); 146 | return configuration 147 | } 148 | } -------------------------------------------------------------------------------- /ui/src/prism/prism-nginx.css: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML 3 | * Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics); 4 | * @author Tim Shedor 5 | */ 6 | 7 | .nginx-config-editor { 8 | counter-reset: line; 9 | } 10 | 11 | .nginx-config-editor textarea { 12 | left: 50px!important; 13 | white-space: pre!important; 14 | } 15 | 16 | .nginx-config-editor #codeArea { 17 | outline: none; 18 | padding-left: 60px !important; 19 | } 20 | 21 | .nginx-config-editor pre { 22 | padding-left: 60px !important; 23 | white-space: pre!important; 24 | } 25 | 26 | .nginx-config-editor .editorLineNumber { 27 | position: absolute; 28 | left: 0; 29 | color: #cccccc; 30 | text-align: right; 31 | width: 40px; 32 | font-weight: 100; 33 | } 34 | 35 | 36 | .nginx-config-editor > code[class*="language-"], 37 | .nginx-config-editor > pre[class*="language-"] { 38 | color: black; 39 | background: none; 40 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 41 | font-size: 1em; 42 | text-align: left; 43 | white-space: pre; 44 | word-spacing: normal; 45 | word-break: normal; 46 | word-wrap: normal; 47 | line-height: 1.5; 48 | 49 | -moz-tab-size: 4; 50 | -o-tab-size: 4; 51 | tab-size: 4; 52 | 53 | -webkit-hyphens: none; 54 | -moz-hyphens: none; 55 | -ms-hyphens: none; 56 | hyphens: none; 57 | } 58 | 59 | /* Code blocks */ 60 | .nginx-config-editor > pre[class*="language-"] { 61 | position: relative; 62 | margin: .5em 0; 63 | overflow: visible; 64 | padding: 1px; 65 | } 66 | 67 | .nginx-config-editor > pre[class*="language-"] > code { 68 | position: relative; 69 | z-index: 1; 70 | border-left: 10px solid #358ccb; 71 | box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf; 72 | background-color: #fdfdfd; 73 | background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); 74 | background-size: 3em 3em; 75 | background-origin: content-box; 76 | background-attachment: local; 77 | } 78 | 79 | .nginx-config-editor > code[class*="language-"] { 80 | max-height: inherit; 81 | height: inherit; 82 | padding: 0 1em; 83 | display: block; 84 | overflow: auto; 85 | } 86 | 87 | /* Margin bottom to accommodate shadow */ 88 | :not(.nginx-config-editor > pre) > .nginx-config-editor > code[class*="language-"], 89 | .nginx-config-editor > pre[class*="language-"] { 90 | background-color: #fdfdfd; 91 | -webkit-box-sizing: border-box; 92 | -moz-box-sizing: border-box; 93 | box-sizing: border-box; 94 | margin-bottom: 1em; 95 | } 96 | 97 | /* Inline code */ 98 | :not(.nginx-config-editor > pre) > .nginx-config-editor > code[class*="language-"] { 99 | position: relative; 100 | padding: .2em; 101 | border-radius: 0.3em; 102 | color: #c92c2c; 103 | border: 1px solid rgba(0, 0, 0, 0.1); 104 | display: inline; 105 | white-space: normal; 106 | } 107 | 108 | .nginx-config-editor > pre[class*="language-"]:before, 109 | .nginx-config-editor > pre[class*="language-"]:after { 110 | content: ''; 111 | display: block; 112 | position: absolute; 113 | bottom: 0.75em; 114 | left: 0.18em; 115 | width: 40%; 116 | height: 20%; 117 | max-height: 13em; 118 | box-shadow: 0px 13px 8px #979797; 119 | -webkit-transform: rotate(-2deg); 120 | -moz-transform: rotate(-2deg); 121 | -ms-transform: rotate(-2deg); 122 | -o-transform: rotate(-2deg); 123 | transform: rotate(-2deg); 124 | } 125 | 126 | .nginx-config-editor > pre[class*="language-"]:after { 127 | right: 0.75em; 128 | left: auto; 129 | -webkit-transform: rotate(2deg); 130 | -moz-transform: rotate(2deg); 131 | -ms-transform: rotate(2deg); 132 | -o-transform: rotate(2deg); 133 | transform: rotate(2deg); 134 | } 135 | 136 | .token.comment, 137 | .token.block-comment, 138 | .token.prolog, 139 | .token.doctype, 140 | .token.cdata { 141 | color: #7D8B99; 142 | } 143 | 144 | .token.punctuation { 145 | color: #5F6364; 146 | } 147 | 148 | .token.property, 149 | .token.tag, 150 | .token.boolean, 151 | .token.number, 152 | .token.function-name, 153 | .token.constant, 154 | .token.symbol, 155 | .token.deleted { 156 | color: #d73038; 157 | } 158 | 159 | .token.selector, 160 | .token.attr-name, 161 | .token.string, 162 | .token.char, 163 | .token.function, 164 | .token.builtin, 165 | .token.inserted { 166 | color: #2f9c0a; 167 | } 168 | 169 | .token.operator, 170 | .token.entity, 171 | .token.url, 172 | .token.variable { 173 | color: #d73038; 174 | background: rgba(255, 255, 255, 0.5); 175 | } 176 | 177 | .token.atrule, 178 | .token.attr-value, 179 | .token.keyword, 180 | .token.class-name { 181 | color: #069; 182 | } 183 | 184 | .token.regex, 185 | .token.important { 186 | color: #e90; 187 | } 188 | 189 | .language-css .token.string, 190 | .style .token.string { 191 | color: #a67f59; 192 | background: rgba(255, 255, 255, 0.5); 193 | } 194 | 195 | .token.important { 196 | font-weight: normal; 197 | } 198 | 199 | .token.bold { 200 | font-weight: bold; 201 | } 202 | 203 | .token.italic { 204 | font-style: italic; 205 | } 206 | 207 | .token.entity { 208 | cursor: help; 209 | } 210 | 211 | .token.namespace { 212 | opacity: .7; 213 | } 214 | 215 | @media screen and (max-width: 767px) { 216 | .nginx-config-editor > pre[class*="language-"]:before, 217 | .nginx-config-editor > pre[class*="language-"]:after { 218 | bottom: 14px; 219 | box-shadow: none; 220 | } 221 | 222 | } 223 | 224 | /* Plugin styles: Line Numbers */ 225 | .nginx-config-editor > pre[class*="language-"].line-numbers.line-numbers { 226 | padding-left: 0; 227 | } 228 | 229 | .nginx-config-editor > pre[class*="language-"].line-numbers.line-numbers code { 230 | padding-left: 3.8em; 231 | } 232 | 233 | .nginx-config-editor > pre[class*="language-"].line-numbers.line-numbers .line-numbers-rows { 234 | left: 0; 235 | } 236 | 237 | /* Plugin styles: Line Highlight */ 238 | .nginx-config-editor > pre[class*="language-"][data-line] { 239 | padding-top: 0; 240 | padding-bottom: 0; 241 | padding-left: 0; 242 | } 243 | 244 | .nginx-config-editor > pre[data-line] code { 245 | position: relative; 246 | padding-left: 4em; 247 | } 248 | 249 | .nginx-config-editor > pre .line-highlight { 250 | margin-top: 0; 251 | } 252 | 253 | span.token.plain { 254 | cursor: pointer; 255 | } 256 | 257 | span.token.plain:hover { 258 | color: #d73038; 259 | font-weight: bold; 260 | } 261 | 262 | 263 | .nginx-banner-error { 264 | color: #fff; 265 | background: repeating-linear-gradient( 266 | 45deg, 267 | #D52536, 268 | #D52536 10px, 269 | #ae303c 10px, 270 | #ae303c 20px 271 | ); 272 | } 273 | -------------------------------------------------------------------------------- /ui/src/configurationUI/Server.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import { 3 | Alert, 4 | Box, 5 | Button, 6 | Grid, 7 | InputAdornment, 8 | TextField, 9 | Typography, 10 | ThemeProvider, 11 | createTheme, Tabs, Tab, 12 | } from "@mui/material"; 13 | import PublishIcon from "@mui/icons-material/Publish"; 14 | import {NetworkService} from "../network/NetworkService"; 15 | import DnsIcon from "@mui/icons-material/Dns"; 16 | import BorderColorIcon from "@mui/icons-material/BorderColor"; 17 | import TabPanel from "@mui/lab/TabPanel"; 18 | import {ConfigurationUi} from "./ConfigurationUi"; 19 | import {ConfigurationEditor} from "../configurationEditor/ConfigurationEditor"; 20 | import {TemplateStore} from "../templateStore/TemplateStore"; 21 | import TabContext from "@mui/lab/TabContext"; 22 | import {Autocomplete} from "@mui/lab"; 23 | import {ConfigurationUiService} from "./ConfigurationUiService"; 24 | 25 | interface ServerProps { 26 | nginxInstance: any 27 | 28 | } 29 | 30 | type ServerConfiguration = { 31 | file: string, 32 | serverName: string | undefined, 33 | listeners: string, 34 | upstream: string | undefined 35 | 36 | } 37 | 38 | export function Server(props: ServerProps) { 39 | 40 | const initServerConfiguration: ServerConfiguration = { 41 | file: "", 42 | serverName: "", 43 | listeners: "", 44 | upstream: "" 45 | 46 | } 47 | 48 | const [state, setState] = useState({}); 49 | const [networkACFields, setNetworkACFields] = useState>([]); 50 | const [tabValue, setTabValue] = useState('1'); 51 | const [serverConfig, setServerConfig] = useState(initServerConfiguration); 52 | 53 | 54 | const inputChangeHandler = (event: React.ChangeEvent) => { 55 | // input field id and value 56 | const {name, value} = event.target 57 | setServerConfig({...serverConfig, [name]: value}) 58 | console.log(serverConfig) 59 | } 60 | const inputConfigFileHandler = (event: React.ChangeEvent) => { 61 | // input field id and value 62 | const {name, value} = event.target 63 | if (value.match("\\/.*$")) { 64 | setServerConfig({...serverConfig, ["file"]: value}) 65 | } else { 66 | setServerConfig({...serverConfig, ["file"]: `/etc/nginx/conf.d/${value}`}) 67 | } 68 | console.log(serverConfig) 69 | } 70 | 71 | const inputChangeHandlerACField = (event: React.ChangeEvent) => { 72 | const upstream = networkACFields[event.target.dataset.optionIndex] 73 | console.log(upstream) 74 | setServerConfig({...serverConfig, upstream: `${upstream.network.ip}:${upstream.ports[0].private}`}) 75 | } 76 | 77 | 78 | useEffect(() => { 79 | const networkService: NetworkService = new NetworkService(); 80 | // Get the available Containers based on the current container network 81 | console.log(props.nginxInstance) 82 | const networkTopology = async () => { 83 | networkService.containersInNetwork(props.nginxInstance.networks[0][0]).then((containers: any) => { 84 | setState(containers) 85 | createACNetworkArray(containers) 86 | }) 87 | } 88 | networkTopology().catch(console.error) 89 | }, []) 90 | 91 | 92 | //Implement Service functions to create names and friends. 93 | const createACNetworkArray = (containers: any) => { 94 | let network: Array = [] 95 | console.log(containers) 96 | containers.map((container: any) => { 97 | let ports: Array = [] 98 | container.Ports.map((portConfig: any) => { 99 | const port = { 100 | private: portConfig.PrivatePort, 101 | public: portConfig.PublicPort || undefined, 102 | type: portConfig.Type 103 | } 104 | ports.push(port) 105 | }) 106 | 107 | const c = { 108 | id: container.Id, 109 | currentActive: (props.nginxInstance.id === container.Id), 110 | name: container.Names[0].split('/')[1], 111 | network: { 112 | ip: container.NetworkSettings.Networks[props.nginxInstance.networks[0][0]].IPAddress 113 | }, 114 | ports: ports 115 | } 116 | network.push(c) 117 | }) 118 | setNetworkACFields(network); 119 | } 120 | 121 | const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { 122 | setTabValue(newValue); 123 | }; 124 | 125 | const createProxyConfiguration = () => { 126 | console.log(serverConfig); 127 | const configUiService = new ConfigurationUiService(); 128 | const configuration = configUiService.createNewServerConfiguration(serverConfig, props.nginxInstance.id); 129 | } 130 | 131 | return ( 132 | 133 | Add a Server 134 | 135 | The new virtual server will be created in a new configuration file. 136 | 137 | 138 | 139 | 140 | } label="Virtual Server" value={"1"}/> 141 | 142 | 143 | 144 | 145 | inputConfigFileHandler(e)} 149 | /> 150 | {serverConfig.file ? (<>{serverConfig.file}) : (<>)} 151 | 152 | 153 | inputChangeHandler(e)} 157 | /> 158 | 159 | 160 | inputChangeHandler(e)} 164 | /> 165 | 166 | 167 | inputChangeHandlerACField(e)} 171 | disableClearable 172 | options={networkACFields.map((client) => `${client.name}:${client.ports[0].private} (${client.ports[0].type})`)} 173 | renderInput={(params) => ( 174 | inputChangeHandler(e)} 180 | InputProps={{ 181 | ...params.InputProps, 182 | type: 'search', 183 | }} 184 | /> 185 | )} 186 | /> 187 | {`http://${serverConfig.upstream}`} 188 | 189 | 194 | 195 | 196 | 197 | 198 | ) 199 | } 200 | 201 | -------------------------------------------------------------------------------- /ui/src/configurationUI/ConfigurationUi.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import {ConfigurationUiService} from "./ConfigurationUiService"; 3 | import { 4 | Box, 5 | Chip, Grid, 6 | IconButton, 7 | Paper, 8 | Slide, 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableContainer, 13 | TableHead, 14 | TableRow, 15 | Tooltip, 16 | Typography 17 | } from "@mui/material"; 18 | import {Add, Close} from "@mui/icons-material"; 19 | import {Location} from "./Location"; 20 | import {Server} from "./Server"; 21 | 22 | interface ConfigurationUiProps { 23 | containerId: string, 24 | nginxInstance: any 25 | } 26 | 27 | export function ConfigurationUi(props: ConfigurationUiProps) { 28 | 29 | const [state, setState] = useState({configuration: {http: {servers: []}}}) 30 | const configurationUiService: any = new ConfigurationUiService() 31 | 32 | useEffect(() => { 33 | const configuration = async () => { 34 | configurationUiService.getConfiguration(props.containerId).then((configuration: any) => { 35 | setState({configuration: configuration}) 36 | }) 37 | } 38 | configuration().catch(console.error) 39 | }, []); 40 | 41 | const renderMountsIfAny: any = () => { 42 | if (props.nginxInstance.mounts.length > 0) { 43 | return ( 44 | 45 | Mounted Volumes 46 | 47 | 48 | 49 | 50 | Type 51 | Source 52 | Destination 53 | 54 | 55 | 56 | {props.nginxInstance.mounts.map((mount: any, index: number) => ( 57 | 61 | 62 | {mount.Type} 63 | 64 | 65 | {mount.Source} 66 | 67 | 68 | {mount.Destination} 69 | 70 | 71 | ))} 72 | 73 |
74 |
75 |
76 | ) 77 | } 78 | } 79 | 80 | const [serverSlide, setServerSlide] = useState(false); 81 | const handleChangeServer = () => { 82 | setServerSlide((prev) => !prev); 83 | }; 84 | 85 | const content = ( 86 | 98 | 108 | 109 | 110 | 111 | 112 | ); 113 | 114 | const [locationSlide, setLocationSlide] = useState(false); 115 | const handleChangeLocation = () => { 116 | setLocationSlide((prev) => !prev); 117 | }; 118 | 119 | const contentLocation = ( 120 | 132 | 143 | 144 | 145 | 146 | 147 | ); 148 | 149 | return ( 150 | <> 151 | 152 | {content} 153 | 154 | 155 | {contentLocation} 156 | 157 | 158 | 159 | 160 | 161 | Server 162 | 163 | 164 | 165 | 166 | 167 | 168 | Configuration File 169 | Ports 170 | Locations 171 | 172 | 173 | 174 | {state.configuration.http.servers.map((server: any, index: number) => ( 175 | 179 | 180 | {server.names.join(',')} 181 | 182 | 183 | 184 | {server.listeners.map((listener: any, index: number) => ( 185 | 186 | ))} 187 | 188 | 189 | {server.locations.map((location: any, index: number) => ( 190 | 191 | ))} 192 | {/**/} 193 | {/* */} 194 | {/* */} 195 | {/* */} 196 | {/**/} 197 | 198 | 199 | ))} 200 | 201 |
202 |
203 | 204 | {renderMountsIfAny()} 205 | 206 | 207 | ) 208 | } 209 | 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /ui/src/configurationEditor/ConfigurationEditor.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | FormControl, 5 | Grid, 6 | InputLabel, 7 | MenuItem, 8 | Select, 9 | SelectChangeEvent, 10 | Slide, 11 | Typography 12 | } from "@mui/material"; 13 | import DataObjectIcon from "@mui/icons-material/DataObject"; 14 | import PublishIcon from "@mui/icons-material/Publish"; 15 | import UndoIcon from "@mui/icons-material/Undo"; 16 | import {useEffect, useState} from "react"; 17 | import {InstancesService} from "../instances/InstancesService"; 18 | import {Add, Close, FileDownload} from "@mui/icons-material"; 19 | import {NewConfigurationFile} from "./NewConfigurationFile"; 20 | import {createDockerDesktopClient} from "@docker/extension-api-client"; 21 | import {Editor} from "../prism/Editor"; 22 | import {Base64} from "js-base64"; 23 | 24 | interface ConfigurationEditorProps { 25 | nginxInstance: any 26 | 27 | } 28 | 29 | //Refactor! Dependency violation! 30 | let instanceService: InstancesService = new InstancesService() 31 | 32 | export function ConfigurationEditor(props: ConfigurationEditorProps) { 33 | const [configFile, setCF] = useState("") 34 | const [fileName, setFileName] = useState("") 35 | const [configurationFileContent, setCFContent] = useState(""); 36 | const [oldConfiguration, setOldConfiguration] = useState(""); 37 | const [configuration, setConfiguration] = useState([]) 38 | const [newConfigurationFileSlide, setNewConfigurationFileSlide] = useState(false); 39 | const [errorClasses, setErrorClasses] = useState({ 40 | bannerBackground: "nginx-banner-neutral", 41 | bannerErrorMessage: "", 42 | backToDashboardDisabled: false, 43 | undoChangesButtonDisabled: true, 44 | }); 45 | 46 | useEffect(() => { 47 | const getConfiguration = async () => { 48 | instanceService.getConfigurations(props.nginxInstance.id).then((data: any) => { 49 | let filesArray: Array = data 50 | filesArray = filesArray.filter(item => item != "").map((item: string) => { 51 | let match = item.match('\\/.*$'); 52 | if (match != undefined) { 53 | return match[0].replace(`:`, ``) 54 | } else { 55 | return "" 56 | } 57 | }) 58 | setConfiguration(filesArray) 59 | }) 60 | } 61 | getConfiguration().catch(console.error) 62 | }, []) 63 | 64 | const configurationFileOnClickHandler: any = (fileName: string) => (event: any) => { 65 | instanceService.getConfigurationFileContent(fileName, props.nginxInstance.id).then((data: any) => { 66 | setOldConfiguration(data.stdout) 67 | setCFContent(data.stdout); 68 | setFileName(fileName); 69 | }).catch((error: any) => console.error()) 70 | 71 | } 72 | const saveConfigurationToFile: any = (file: string, containerId: string) => (event: any) => { 73 | //build dynamically from TextInput as B64. 74 | const content = Base64.encode(configurationFileContent) 75 | instanceService.sendConfigurationToFile(file, containerId, content).then((data: any) => { 76 | //error handling here. 77 | instanceService.reloadNGINX(containerId).then((data: any) => { 78 | instanceService.displaySuccessMessage("Configuration successfully updated!"); 79 | setErrorClasses({ 80 | bannerBackground: "nginx-banner-neutral", 81 | backToDashboardDisabled: false, 82 | undoChangesButtonDisabled: true, 83 | bannerErrorMessage: "" 84 | }); 85 | }).catch((reason: any) => { 86 | instanceService.displayErrorMessage(`Error while updating configuration: ${reason.stderr.split("\n")[1]}`); 87 | setErrorClasses({ 88 | bannerBackground: "nginx-banner-error", 89 | backToDashboardDisabled: true, 90 | undoChangesButtonDisabled: false, 91 | bannerErrorMessage: `Error while updating configuration: ${reason.stderr.split("\n")[1]}` 92 | }); 93 | }) 94 | }) 95 | } 96 | const nginxConfigurationFileOnChangeHandler = (event: SelectChangeEvent) => { 97 | setCF(event.target.value as string); 98 | setFileName(event.target.value as string); 99 | instanceService.getConfigurationFileContent(event.target.value as string, props.nginxInstance.id).then((data: any) => { 100 | setOldConfiguration(data.stdout) 101 | setCFContent(data.stdout); 102 | }).catch((error: any) => console.error()) 103 | }; 104 | const undoChanges: any = () => { 105 | setCFContent(oldConfiguration) 106 | const content = Base64.encode(oldConfiguration) 107 | instanceService.sendConfigurationToFile(fileName, props.nginxInstance.id, content).then((data: any) => { 108 | //error handling here. 109 | instanceService.reloadNGINX(props.nginxInstance.id).then((data: any) => { 110 | instanceService.displaySuccessMessage("Configuration rollback successful"); 111 | setErrorClasses({ 112 | bannerBackground: "nginx-banner-neutral", 113 | backToDashboardDisabled: false, 114 | undoChangesButtonDisabled: true, 115 | bannerErrorMessage: "" 116 | }); 117 | }).catch((reason: any) => { 118 | instanceService.displayErrorMessage("Error while rolling back configuration! Contact Support!"); 119 | setErrorClasses({ 120 | bannerBackground: "nginx-banner-error", 121 | backToDashboardDisabled: true, 122 | undoChangesButtonDisabled: false, 123 | bannerErrorMessage: `Error while updating configuration: ${reason.stderr.split("\n")[1]}` 124 | }); 125 | }) 126 | }) 127 | } 128 | 129 | const handleNewConfigurationFileSlide = () => { 130 | setNewConfigurationFileSlide((prev) => !prev); 131 | }; 132 | 133 | const content = ( 134 | 146 | 156 | 157 | 158 | 159 | 160 | ); 161 | 162 | const handleExportConfigurationFile = async () => { 163 | let ddClient = createDockerDesktopClient(); 164 | 165 | const result: any = await ddClient.desktopUI.dialog.showOpenDialog({ 166 | properties: ["openDirectory"], 167 | }); 168 | } 169 | 170 | return ( 171 | 172 | 173 | {content} 174 | 175 | 176 | 177 | Configuration File 178 | 190 | 191 | 192 | {!configurationFileContent ? ( 193 | 194 | 195 | 196 | 197 | 198 | Please select a configuration file 199 | 200 | 201 | 205 | 206 | 207 | ) : ( 208 | 209 | 210 | 211 | {fileName} 212 | 213 | 215 | 218 | {/**/} 222 | 228 | 229 | 230 | Configuration Editor 231 | 233 | 234 | 235 | )} 236 | 237 | ) 238 | } 239 | -------------------------------------------------------------------------------- /ui/src/instances/NginxInstances.tsx: -------------------------------------------------------------------------------- 1 | import React, {MouseEventHandler, useEffect, useState} from 'react'; 2 | import {InstancesService} from "./InstancesService"; 3 | import { 4 | Box, 5 | Grid, 6 | IconButton, 7 | Tab, 8 | Tabs, 9 | Tooltip, 10 | ThemeProvider, 11 | Typography, 12 | createTheme, Button, 13 | } from "@mui/material"; 14 | import "./Instance.css"; 15 | 16 | import { 17 | ArrowBackIosNewOutlined 18 | } from "@mui/icons-material"; 19 | 20 | import DnsIcon from '@mui/icons-material/Dns'; 21 | import BorderColorIcon from '@mui/icons-material/BorderColor'; 22 | import TabContext from '@mui/lab/TabContext'; 23 | import TabPanel from '@mui/lab/TabPanel'; 24 | import {ConfigurationUi} from "../configurationUI/ConfigurationUi"; 25 | import {ConfigurationEditor} from "../configurationEditor/ConfigurationEditor"; 26 | import {TemplateStore} from "../templateStore/TemplateStore"; 27 | 28 | 29 | interface NginxInstances { 30 | id: string, 31 | name: string, 32 | mounts: Array, 33 | networks: Array 34 | } 35 | 36 | export function NginxInstance() { 37 | //Refactoring - Make the state more inclusive 38 | // - (merge ContainerId, ConfigurationFile and ConfigurationFileContent) in a single state property. 39 | // 40 | const [instances, setResponse] = useState([]); 41 | const [containerId, setContainerId] = useState(undefined); 42 | const [loading, setLoading] = useState(true); 43 | 44 | const [errorClasses, setErrorClasses] = useState({ 45 | bannerBackground: "nginx-banner-neutral", 46 | bannerErrorMessage: "", 47 | backToDashboardDisabled: false, 48 | undoChangesButtonDisabled: true 49 | }); 50 | 51 | //new State Object - old stuff has to be refactored! 52 | const [nginxInstance, setNginxInstance] = useState({id: "", name: "", mounts: [], networks: []}) 53 | // Holds the "original" Configuration before modifying to be able to role-back in case of errors. 54 | 55 | const instanceService: InstancesService = new InstancesService() 56 | 57 | useEffect(() => { 58 | const instancePromise = async () => { 59 | instanceService.getInstances().then((data: any) => { 60 | const instancesArray: Array = [] 61 | data.map(async (inst: any, index: number) => { 62 | const container = await Promise.resolve(inst.promise).catch((reason: any) => { 63 | }) 64 | if (container != undefined && !container.code) { 65 | instancesArray.push({ 66 | id: inst.container, 67 | out: container.stderr, 68 | ports: inst.ports, 69 | networks: inst.networks, 70 | mounts: inst.mounts, 71 | status: inst.status, 72 | name: inst.name.replace("/", "") 73 | }) 74 | } 75 | }); 76 | setResponse(instancesArray) 77 | setLoading(false) 78 | console.log(instancesArray) 79 | }); 80 | } 81 | 82 | instancePromise().catch(console.error) 83 | }, []); 84 | 85 | const nginxInstanceOnClickHandler: MouseEventHandler | any = (containerId: string, name: string) => (event: MouseEventHandler) => { 86 | instanceService.getConfigurations(containerId).then((data: any) => { 87 | const mounts = instances.find(({id}: any) => id === containerId).mounts || [] 88 | const networks = instances.find(({id}: any) => id === containerId).networks || [] 89 | 90 | setNginxInstance({id: containerId, name: name, mounts: mounts, networks: networks}) 91 | }) 92 | } 93 | 94 | //Refactoring! Move this into a separate component 95 | const [tabValue, setTabValue] = useState('1'); 96 | const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { 97 | setTabValue(newValue); 98 | }; 99 | 100 | const containerNetwork: any = (port: any, key: number) => { 101 | if (port.PrivatePort && port.PublicPort) { 102 | return ( 103 | 104 | {`${port.PublicPort}:${port.PrivatePort} (${port.Type})`} 105 | 106 | ) 107 | } 108 | if (port.PrivatePort && !port.PublicPort) { 109 | return ( 110 | 111 | {`unbound:${port.PrivatePort} (${port.Type})`} 112 | 113 | ) 114 | } 115 | } 116 | 117 | const renderErrorMessageIfAny = () => { 118 | if (errorClasses.undoChangesButtonDisabled === false) { 119 | return ( 120 | 121 | {errorClasses.bannerErrorMessage}. Click "Undo Changes"! 122 | 123 | ) 124 | } 125 | } 126 | 127 | return ( 128 | 129 | {!loading ? ( 130 | !nginxInstance.id ? ( 131 | instances == 0 ? ( 132 | 133 | There are no active NGINX containers! Start a container to get started! 134 | {/* @ts-expect-error not typed yet! */} 135 | docker run -d -P 136 | nginx:latest 137 | ) : ( 138 | 139 | Active containers running NGINX 140 | { 141 | instances.map((inst: any, key: number) => ( 142 | 143 | 148 | {inst.name} 149 | Container ID: 150 | {inst.id.substring(0, 12)} 152 | 153 | Status: 154 | {inst.status.toLowerCase()} 156 | 157 | 158 | NGINX 159 | Version: 160 | 162 | {inst.out.substring(15, inst.out.length)} 163 | 164 | 165 | 166 | Network: 167 | {inst.networks.map((network: any, key: number) => ( 168 | {network[0]} - {network[1]['IPAddress']} 170 | ))} 171 | 172 | 173 | Open Ports 174 | (Host:Container): 175 | {inst.ports.map((port: any, key: number) => containerNetwork(port, key))} 176 | 177 | {inst.mounts.length > 0 ? ( 178 | 179 | Number of Mounted 180 | Volumes: 181 | {inst.mounts.length} 183 | 184 | ) : ("")} 185 | 186 | 187 | ))} 188 | 189 | 190 | ) 191 | ) : ( 192 | 193 | 194 | 195 | 196 | { 197 | setContainerId(undefined) 198 | setNginxInstance({id: "", name: "", mounts: [], networks: []}) 199 | }} disabled={errorClasses.backToDashboardDisabled}> 200 | 201 | 202 | 203 | {nginxInstance.name} 204 | 205 | 206 | Container ID: {nginxInstance.id.substring(0, 12)} 207 | 208 | {renderErrorMessageIfAny()} 209 | 210 | 211 | 212 | 213 | 214 | } label="Servers" value={"1"}/> 215 | } label="Configuration Editor" value={"2"}/> 216 | {/*} label="Templates Store" value={"3"}/>*/} 217 | {/*} label="Export Configuration" value={"4"}/>*/} 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | <>Exports 231 | 232 | 233 | 234 | ) 235 | ) : (Loading...)} 236 | 237 | ); 238 | } 239 | --------------------------------------------------------------------------------