├── 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 | }
52 | onClick={() => {}}
53 | style={{marginLeft: "0.5rem"}}>Publish
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 | }
55 | onClick={saveConfigurationToFile}
56 | style={{marginLeft: "0.5rem"}}>Publish
57 |
58 |
59 | )
60 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://securityscorecards.dev/viewer/?uri=github.com/nginx/docker-extension)
2 | [](https://www.repostatus.org/#active)
3 | [](/SUPPORT.md)
4 | [](https://community.nginx.org)
5 | [](https://opensource.org/license/apache-2-0)
6 | [](/CODE_OF_CONDUCT.md)
7 |
8 | # NGINX Docker Desktop Extension
9 |
10 | 
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 | }
192 | onClick={createProxyConfiguration}>Publish
193 |
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 |
185 | {configuration.map((file: string, index: number) => (
186 | {file}
187 | ))
188 | }
189 |
190 |
191 |
192 | {!configurationFileContent ? (
193 |
194 |
195 |
196 |
197 |
198 | Please select a configuration file
199 |
200 |
201 | }>
203 | Create New Configuration File
204 |
205 |
206 |
207 | ) : (
208 |
209 |
210 |
211 | {fileName}
212 |
213 | }
214 | onClick={handleNewConfigurationFileSlide}>New File
215 | }
216 | onClick={saveConfigurationToFile(fileName, props.nginxInstance.id)}
217 | style={{marginLeft: "0.5rem"}}>Publish
218 | {/*}*/}
219 | {/* style={{marginLeft: "0.5rem"}}*/}
220 | {/* onClick={handleExportConfigurationFile}*/}
221 | {/*>Export File */}
222 | }
223 | onClick={undoChanges}
224 | color={"error"}
225 | style={{marginLeft: "0.5rem"}}
226 | disabled={errorClasses.undoChangesButtonDisabled}
227 | >Undo Changes
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 |
--------------------------------------------------------------------------------