├── 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 ├── .github ├── CODEOWNERS ├── scorecard.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── pull_request_template.md └── workflows │ ├── ossf_scorecard.yml │ └── f5_cla.yml ├── metadata.json ├── logo.svg ├── SECURITY.md ├── Makefile ├── Dockerfile ├── SUPPORT.md ├── 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/nginx/docker-extension/HEAD/docs/NGINX-dd-extension.png -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | ##################### 2 | # Main global owner # 3 | ##################### 4 | 5 | * @nginx/docker-extension 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/scorecard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | annotations: 3 | - checks: 4 | - fuzzing 5 | - packaging 6 | - sast 7 | - signed-releases 8 | reasons: 9 | - reason: not-applicable 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 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: 💬 Talk to the NGINX community! 5 | url: https://community.nginx.org 6 | about: A community forum for NGINX users, developers, and contributors 7 | - name: 📝 Code of Conduct 8 | url: https://www.contributor-covenant.org/version/2/1/code_of_conduct 9 | about: NGINX follows the Contributor Covenant Code of Conduct to ensure a safe and inclusive community 10 | - name: 💼 For commercial & enterprise users 11 | url: https://www.f5.com/products/nginx 12 | about: F5 offers a wide range of NGINX products for commercial & enterprise users 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Latest Versions 4 | 5 | We advise users to run or update to the most recent release of this project. Older versions of this project may not have all enhancements and/or bug fixes applied to them. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | The F5 Security Incident Response Team (F5 SIRT) offers two methods to easily report potential security vulnerabilities: 10 | 11 | - If you’re an F5 customer with an active support contract, please contact [F5 Technical Support](https://www.f5.com/support). 12 | - If you aren’t an F5 customer, please report any potential or current instances of security vulnerabilities in any F5 product to the F5 Security Incident Response Team at . 13 | 14 | For more information, please read the F5 SIRT vulnerability reporting guidelines available at [https://www.f5.com/support/report-a-vulnerability](https://www.f5.com/support/report-a-vulnerability). 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Proposed changes 2 | 3 | Describe the use case and detail of the change. If this PR addresses an issue on GitHub, make sure to include a link to that issue using one of the [supported keywords](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) in this PR's description or commit message. 4 | 5 | ### Checklist 6 | 7 | Before creating a PR, run through this checklist and mark each as complete: 8 | 9 | - [ ] I have read the [contributing guidelines](/CONTRIBUTING.md). 10 | - [ ] I have signed the [F5 Contributor License Agreement (CLA)](https://github.com/f5/f5-cla/blob/main/docs/f5_cla.md). 11 | - [ ] If applicable, I have added tests that prove my fix is effective or that my feature works. 12 | - [ ] If applicable, I have checked that any relevant tests pass after adding my changes. 13 | - [ ] I have updated any relevant documentation ([`README.md`](/README.md)). 14 | -------------------------------------------------------------------------------- /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.4", 9 | "@emotion/react": "^11.14.0", 10 | "@emotion/styled": "^11.14.0", 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.5", 15 | "js-base64": "^3.7.7", 16 | "prism-react-renderer": "^1.3.5", 17 | "prismjs": "^1.30.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.4", 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": "^4.4.1", 34 | "jest": "^29.7.0", 35 | "typescript": "^4.8.3", 36 | "vite": "^6.3.2" 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:22.14-alpine3.21 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.4" \ 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="Chore: Updated Node dependencies" 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 | } -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## Ask a Question 4 | 5 | We use GitHub for tracking bugs and feature requests related to this project. 6 | 7 | Don't know how something in this project works? Curious if this project can achieve your desired functionality? Please open an issue on GitHub with the label `question`. Alternatively, start a GitHub discussion! 8 | 9 | ## NGINX Specific Questions and/or Issues 10 | 11 | This isn't the right place to get support for NGINX specific questions, but the following resources are available below. Thanks for your understanding! 12 | 13 | ### Community Forum 14 | 15 | We have a community [forum](https://community.nginx.org/)! If you have any questions and/or issues, try checking out the [`Troubleshooting`](https://community.nginx.org/c/troubleshooting/8) and [`How do I...?`](https://community.nginx.org/c/how-do-i/9) categories. Both fellow community members and NGINXers might be able to help you! :) 16 | 17 | ### Documentation 18 | 19 | For a comprehensive list of all NGINX directives, check out . 20 | 21 | For a comprehensive list of administration and deployment guides for all NGINX products, check out . 22 | 23 | ### Mailing List 24 | 25 | Want to get in touch with the NGINX development team directly? Try using the relevant mailing list found at ! 26 | 27 | ## Contributing 28 | 29 | Please see the [contributing guide](/CONTRIBUTING.md) for guidelines on how to best contribute to this project. 30 | 31 | ## Community Support 32 | 33 | This project does **not** offer commercial support. Community support is offered on a best effort basis through either GitHub issues/PRs/discussions or through any of our active communities. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | description: Suggest an idea for this project 4 | labels: enhancement 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this feature request! 10 | 11 | Before you continue filling out this request, please take a moment to check that your feature has not been [already requested on GitHub][issue search] 🙌 12 | 13 | **Note:** If you are seeking community support or have a question, please consider starting a new thread via [GitHub discussions][discussions] or the [NGINX Community forum][forum]. 14 | 15 | [issue search]: https://github.com/nginx/docker-extension/issues 16 | [discussions]: https://github.com/nginx/docker-extension/discussions 17 | [forum]: https://community.nginx.org 18 | 19 | - type: textarea 20 | id: overview 21 | attributes: 22 | label: Feature Overview 23 | description: A clear and concise description of what the feature request is. 24 | placeholder: I would like this project to be able to do "X". 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: alternatives 30 | attributes: 31 | label: Alternatives Considered 32 | description: Detail any potential alternative solutions/workarounds you've used or considered. 33 | placeholder: I have done/might be able to do "X" in this project by doing "Y". 34 | 35 | - type: textarea 36 | id: context 37 | attributes: 38 | label: Additional Context 39 | description: Add any other context about the problem here. 40 | placeholder: Feel free to add any other context/information/screenshots/etc... that you think might be relevant to this feature request here. 41 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.github/workflows/ossf_scorecard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow uses actions that are not certified by GitHub. They are provided by a third-party and are governed by separate terms of service, privacy policy, and support documentation. 3 | name: OSSF Scorecard 4 | on: 5 | # For Branch-Protection check. Only the default branch is supported. See https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection. 6 | branch_protection_rule: 7 | # To guarantee Maintained check is occasionally updated. See https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained. 8 | schedule: 9 | - cron: "0 0 * * 1" 10 | push: 11 | branches: [main] 12 | workflow_dispatch: 13 | # Declare default permissions as read only. 14 | permissions: read-all 15 | jobs: 16 | analysis: 17 | name: Scorecard analysis 18 | runs-on: ubuntu-24.04 19 | permissions: 20 | # Needed if using Code Scanning alerts. 21 | security-events: write 22 | # Needed for GitHub OIDC token if publish_results is true. 23 | id-token: write 24 | steps: 25 | - name: Check out the codebase 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Run analysis 31 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 32 | with: 33 | results_file: results.sarif 34 | results_format: sarif 35 | publish_results: true 36 | 37 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF format to the repository Actions tab. 38 | - name: Upload artifact 39 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 40 | with: 41 | name: SARIF file 42 | path: results.sarif 43 | retention-days: 5 44 | 45 | # Upload the results to GitHub's code scanning dashboard. 46 | - name: Upload SARIF results to code scanning 47 | uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 48 | with: 49 | sarif_file: results.sarif 50 | -------------------------------------------------------------------------------- /.github/workflows/f5_cla.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: F5 CLA 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_target: 7 | types: [opened, closed, synchronize] 8 | permissions: read-all 9 | jobs: 10 | f5-cla: 11 | name: F5 CLA 12 | runs-on: ubuntu-24.04 13 | permissions: 14 | actions: write 15 | pull-requests: write 16 | statuses: write 17 | steps: 18 | - name: Run F5 Contributor License Agreement (CLA) assistant 19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have hereby read the F5 CLA and agree to its terms') || github.event_name == 'pull_request_target' 20 | uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 21 | with: 22 | # Path to the CLA document. 23 | path-to-document: https://github.com/f5/f5-cla/blob/main/docs/f5_cla.md 24 | # Custom CLA messages. 25 | custom-notsigned-prcomment: '🎉 Thank you for your contribution! It appears you have not yet signed the [F5 Contributor License Agreement (CLA)](https://github.com/f5/f5-cla/blob/main/docs/f5_cla.md), which is required for your changes to be incorporated into an F5 Open Source Software (OSS) project. Please kindly read the [F5 CLA](https://github.com/f5/f5-cla/blob/main/docs/f5_cla.md) and reply on a new comment with the following text to agree:' 26 | custom-pr-sign-comment: 'I have hereby read the F5 CLA and agree to its terms' 27 | custom-allsigned-prcomment: '✅ All required contributors have signed the F5 CLA for this PR. Thank you!' 28 | # Remote repository storing CLA signatures. 29 | remote-organization-name: f5 30 | remote-repository-name: f5-cla-data 31 | # Branch where CLA signatures are stored. 32 | branch: main 33 | path-to-signatures: signatures/signatures.json 34 | # Comma separated list of usernames for maintainers or any other individuals who should not be prompted for a CLA. 35 | # NOTE: You will want to edit the usernames to suit your project needs. 36 | allowlist: bot* 37 | # Do not lock PRs after a merge. 38 | lock-pullrequest-aftermerge: false 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | PERSONAL_ACCESS_TOKEN: ${{ secrets.F5_CLA_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | description: Create a report to help us improve 4 | labels: bug 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | Before you continue filling out this report, please take a moment to check that your bug has not been [already reported on GitHub][issue search] 🙌 12 | 13 | Remember to redact any sensitive information such as authentication credentials and/or license keys! 14 | 15 | **Note:** If you are seeking community support or have a question, please consider starting a new thread via [GitHub discussions][discussions] or the [NGINX Community forum][forum]. 16 | 17 | [issue search]: https://github.com/nginx/docker-extension/issues 18 | [discussions]: https://github.com/nginx/docker-extension/discussions 19 | [forum]: https://community.nginx.org 20 | 21 | - type: textarea 22 | id: overview 23 | attributes: 24 | label: Bug Overview 25 | description: A clear and concise overview of the bug. 26 | placeholder: When I use the NGINX Docker extension to do "X", "Y" happens instead of "Z". 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: behavior 32 | attributes: 33 | label: Expected Behavior 34 | description: A clear and concise description of what you expected to happen. 35 | placeholder: When I use the NGINX Docker extension to do "X", I expect "Z" to happen. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: steps 41 | attributes: 42 | label: Steps to Reproduce the Bug 43 | description: Detail the series of steps required to reproduce the bug. 44 | placeholder: When I use the NGINX Docker extension to run "X" using [...], "X" fails with "Y" error message. If I check the terminal outputs and/or logs, I see the following info. 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: environment 50 | attributes: 51 | label: Environment Details 52 | description: Please provide details about your environment. 53 | value: | 54 | - Target deployment platform: [e.g. AWS/GCP/local cluster/etc...] 55 | - Target OS: [e.g. RHEL 9/Ubuntu 24.04/etc...] 56 | - Version of this project or specific commit: [e.g. 1.4.3/commit hash] 57 | - Version of any relevant project languages: [e.g. Kubernetes 1.30/Python 3.9.7/etc...] 58 | validations: 59 | required: true 60 | 61 | - type: textarea 62 | id: context 63 | attributes: 64 | label: Additional Context 65 | description: Add any other context about the problem here. 66 | placeholder: Feel free to add any other context/information/screenshots/etc... that you think might be relevant to this issue in here. 67 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/nginx/docker-extension/badge)](https://securityscorecards.dev/viewer/?uri=github.com/nginx/docker-extension) 2 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 3 | [![Community Support](https://badgen.net/badge/support/community/cyan?icon=awesome)](/SUPPORT.md) 4 | [![Community Forum](https://img.shields.io/badge/community-forum-009639?logo=discourse&link=https%3A%2F%2Fcommunity.nginx.org)](https://community.nginx.org) 5 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/license/apache-2-0) 6 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](/CODE_OF_CONDUCT.md) 7 | 8 | # NGINX Docker Desktop Extension 9 | 10 | ![NGINX Docker Extension Instance Screen](docs/NGINX-dd-extension.png) 11 | 12 | The NGINX Docker Desktop Extension can be used to manage the instance configuration of a running NGINX container. 13 | 14 | ## Development 15 | Before we can interactively develop the Extensions frontend, it must be installed first. 16 | 17 | To build the extension locally. 18 | ```shell 19 | docker build -t nginx/nginx-dd-extension . 20 | ``` 21 | To install the extension 22 | ```shell 23 | docker extension install nginx/nginx-dd-extension 24 | ``` 25 | 26 | To remove the extension 27 | ```shell 28 | docker remove nginx/nginx-dd-extension 29 | ``` 30 | ## Release 31 | 32 | ```shell 33 | docker buildx build --push --no-cache --platform=linux/amd64,linux/arm64 -t nginx/nginx-docker-extension:0.0.1 . 34 | ``` 35 | 36 | ### Start Docker Extension Development Server 37 | 1. start the UI node server in the `ui` directory. Make sure you install the dev dependencies at the first. 38 | ```shell 39 | npm install 40 | npm run dev 41 | ``` 42 | 43 | 2. enable debugging for the NGINX Docker Extension. 44 | ```shell 45 | docker extension dev debug nginx/nginx-dd-extension 46 | ``` 47 | 48 | ```shell 49 | docker extension dev ui-source nginx/nginx-dd-extension http://localhost:3000 50 | ``` 51 | ## Community 52 | 53 | - The go-to place to start asking questions and share your thoughts is 54 | our [Slack channel](https://community.nginx.org/joinslack). 55 | 56 | - Get involved with the project by contributing! See the 57 | [contributing guide](CONTRIBUTING.md) for details. 58 | 59 | - For security issues, [email us](security-alert@nginx.org), mentioning 60 | NGINX Unit in the subject and following the [CVSS 61 | v3.1](https://www.first.org/cvss/v3.1/specification-document) spec. 62 | 63 | 64 | ## Backlog 65 | 66 | ### Re-Expose new Ports 67 | ```shell 68 | docker commit CONTAINERID NEWIMAGE 69 | docker run NEWIMAGE -p ... -p.... -v POSSIBLE MOUNTS 70 | ``` 71 | ### Export Configuration 72 | Export configuration files from inside the container to a projects directory on the local computer 73 | ```shell 74 | docker cp CONTAINERID:/etc/nginx/conf.d/test.conf ./something/.... 75 | ``` 76 | ## Contributing 77 | 78 | Please see the [contributing guide](/CONTRIBUTING.md) for guidelines on how to best contribute to this project. 79 | 80 | ## License 81 | 82 | [Apache License, Version 2.0](/LICENSE) 83 | 84 | © [F5, Inc.](https://www.f5.com/) 2023 - 2025 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | The following is a set of guidelines for contributing to this project. We really appreciate that you are considering contributing! 4 | 5 | #### Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Contributing](#contributing) 9 | - [Code Guidelines](#code-guidelines) 10 | - [Code of Conduct](/CODE_OF_CONDUCT.md) 11 | 12 | ## Getting Started 13 | 14 | Follow the instructions on the README's [Getting Started](/README.md#Getting-Started) section to get this project up and running. 15 | 16 | ## Contributing 17 | 18 | ### Report a Bug 19 | 20 | To report a bug, open an issue on GitHub with the label `bug` using the available [bug report issue form](/.github/ISSUE_TEMPLATE/bug_report.yml). Please ensure the bug has not already been reported. **If the bug is a potential security vulnerability, please report it using our [security policy](/SECURITY.md).** 21 | 22 | ### Suggest a Feature or Enhancement 23 | 24 | To suggest a feature or enhancement, please create an issue on GitHub with the label `enhancement` using the available [feature request issue form](/.github/ISSUE_TEMPLATE/feature_request.yml). Please ensure the feature or enhancement has not already been suggested. 25 | 26 | ### Open a Pull Request (PR) 27 | 28 | - Fork the repo, create a branch, implement your changes, add any relevant tests, and submit a PR when your changes are **tested** and ready for review. 29 | - Fill in the [PR template](/.github/pull_request_template.md). 30 | 31 | **Note:** If you'd like to implement a new feature, please consider creating a [feature request issue](/.github/ISSUE_TEMPLATE/feature_request.yml) first to start a discussion about the feature. 32 | 33 | #### F5 Contributor License Agreement (CLA) 34 | 35 | F5 requires all contributors to agree to the terms of the F5 CLA (available [here](https://github.com/f5/f5-cla/.github/blob/main/docs/f5_cla.md)) before any of their changes can be incorporated into an F5 Open Source repository (even contributions to the F5 CLA itself!). 36 | 37 | If you have not yet agreed to the F5 CLA terms and submit a PR to this repository, a bot will prompt you to view and agree to the F5 CLA. You will have to agree to the F5 CLA terms through a comment in the PR before any of your changes can be merged. Your agreement signature will be safely stored by F5 and no longer be required in future PRs. 38 | 39 | ## Code Guidelines 40 | 41 | 42 | 43 | ### Git Guidelines 44 | 45 | - Keep a clean, concise and meaningful git commit history on your branch (within reason), rebasing locally and squashing before submitting a PR. 46 | - If possible and/or relevant, use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format when writing a commit message, so that changelogs can be automatically generated. 47 | - Follow the guidelines of writing a good commit message as described here and summarized in the next few points: 48 | - In the subject line, use the present tense ("Add feature" not "Added feature"). 49 | - In the subject line, use the imperative mood ("Move cursor to..." not "Moves cursor to..."). 50 | - Limit the subject line to 72 characters or less. 51 | - Reference issues and pull requests liberally after the subject line. 52 | - Add more detailed description in the body of the git message (`git commit -a` to give you more space and time in your text editor to write a good message instead of `git commit -am`). 53 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people. 14 | - Being respectful of differing opinions, viewpoints, and experiences. 15 | - Giving and gracefully accepting constructive feedback. 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience. 17 | - Focusing on what is best not just for us as individuals, but for the overall community. 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind. 22 | - Trolling, insulting or derogatory comments, and personal or political attacks. 23 | - Public or private harassment. 24 | - Publishing others' private information, such as a physical or email address, without their explicit permission. 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting. 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at . 74 | 75 | Community Impact Guidelines were inspired by 76 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/inclusion). 77 | 78 | For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . 79 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------