Browse Source

re-implemented the install and uninstall functions

lllllllillllllillll 1 year ago
parent
commit
40bd0b693c
8 changed files with 265 additions and 297 deletions
  1. 2 1
      .gitignore
  2. 1 3
      components/appCard.js
  3. 78 0
      components/containerCard.js
  4. 1 1
      controllers/apps.js
  5. 163 187
      functions/install.js
  6. 19 21
      functions/uninstall.js
  7. 1 2
      router/index.js
  8. 0 82
      views/dashboard.ejs

+ 2 - 1
.gitignore

@@ -2,4 +2,5 @@ node_modules
 database/database.sqlite
 database/database.sqlite
 test
 test
 .dockerignore
 .dockerignore
-.gitignore
+.gitignore
+appdata

+ 1 - 3
components/appCard.js

@@ -1,6 +1,5 @@
 export const appCard = (data) => {
 export const appCard = (data) => {
 
 
-  // make data.title lowercase
   let app_name = data.name || data.title.toLowerCase();
   let app_name = data.name || data.title.toLowerCase();
   let shortened_name = "";
   let shortened_name = "";
   let shortened_desc = data.description.slice(0, 60) + "...";
   let shortened_desc = data.description.slice(0, 60) + "...";
@@ -15,11 +14,10 @@ export const appCard = (data) => {
   let repository = data.repository || "";
   let repository = data.repository || "";
   let source = data.image || "";
   let source = data.image || "";
 
 
-
-  // if data.network is set to host, bridge, or docker set the radio button to checked
   let net_host, net_bridge, net_docker = '';
   let net_host, net_bridge, net_docker = '';
   let net_name = 'AppBridge';
   let net_name = 'AppBridge';
   
   
+  // if data.network is set to host, bridge, or docker set the radio button to checked
   if (data.network == 'host') {
   if (data.network == 'host') {
     net_host = 'checked';
     net_host = 'checked';
   } else if (data.network) {
   } else if (data.network) {

+ 78 - 0
components/containerCard.js

@@ -114,5 +114,83 @@ export const containerCard = (data) => {
           <div id="${chart}_chart" class="chart-sm"></div>
           <div id="${chart}_chart" class="chart-sm"></div>
         </div>
         </div>
       </div>
       </div>
+    </div>
+    <div class="modal modal-blur fade" id="remove_modal" tabindex="-1" style="display: none;" aria-hidden="true">
+      <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
+        <div class="modal-content">
+          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+          <div class="modal-status bg-danger"></div>
+          <div class="modal-body text-center py-3">
+            <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
+            <h3>Remove ${name}?</h3>
+            <form action="/uninstall" id="${name}_uninstall" method="POST">
+            <input type="text" class="form-control" name="service_name" value="${name}" hidden/>
+            <div class="mb-3"> </div>
+            
+            <div class="mb-2">
+              <div class="divide-y">
+                <div class="row">
+                  <div class="col-9">
+                    <label class="row text-start">
+                      <span class="col">Remove Volumes</span>
+                    </label>
+                  </div>
+                  <div class="col-3">
+                    <label class="form-check form-check-single form-switch text-end">
+                      <input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
+                    </label>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-9">
+                    <label class="row text-start">
+                      <span class="col">
+                        Remove Image
+                      </span>
+                    </label>
+                  </div>
+                  <div class="col-3">
+                    <label class="form-check form-check-single form-switch text-end">
+                      <input class="form-check-input" type="checkbox" name="remove_image" disabled="">
+                    </label>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-9">
+                    <label class="row text-start">
+                      <span class="col">
+                        Remove Backups
+                      </span>
+                    </label>
+                  </div>
+                  <div class="col-3">
+                    <label class="form-check form-check-single form-switch text-end">
+                      <input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
+                    </label>
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div class="mt-1"> </div>
+            <div class="text-muted">Enter "Yes" below to remove the container.</div>
+            <input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
+            </form>
+          </div>
+          <div class="modal-footer">
+            <div class="w-100">
+              <div class="row">
+                <div class="col">
+                  <a href="#" class="btn w-100" data-bs-dismiss="modal">
+                    Cancel
+                  </a>
+                </div>
+                <div class="col">
+                  <input type="submit" form="${name}_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
     </div>`;
     </div>`;
 }
 }

+ 1 - 1
controllers/apps.js

@@ -51,9 +51,9 @@ export const Apps = (req, res) => {
 
 
 export const appSearch = async (req, res) => {
 export const appSearch = async (req, res) => {
 
 
+    let search = req.body.search.split(' ');
     let apps_list = '';
     let apps_list = '';
     let results = [];
     let results = [];
-    let search = req.body.search.split(' ');
 
 
     let page = Number(req.query.page) || 1;
     let page = Number(req.query.page) || 1;
     let list_start = (page - 1) * 28;
     let list_start = (page - 1) * 28;

+ 163 - 187
functions/install.js

@@ -8,196 +8,172 @@ import DockerodeCompose from "dockerode-compose";
 
 
 export const Install = async (req, res) => {
 export const Install = async (req, res) => {
 
 
-    console.log('Install:')
-    console.log(req.body);
+        let data = req.body;
+
+        let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;        
+        let { port0, port1, port2, port3, port4, port5 } = data;
+        let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
+        let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
+        let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
+
+        let ports = [port0, port1, port2, port3, port4, port5]
+
+        let docker_volumes = [];
+
+        if (image.startsWith('https://')){
+            mkdirSync(`./appdata/${name}`, { recursive: true });
+            execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
+            console.log(`Downloaded stackfile: ${image}`);
+            let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
+            let services = Object.keys(stackfile.services);
+
+            for ( let i = 0; i < services.length; i++ ) {
+                try {
+                    console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
+                } catch { console.log('no env') }
+            }
+            
+        } else {
+
+            let compose_file = `version: '3'`;
+                compose_file += `\nservices:`
+                compose_file += `\n  ${service_name}:`
+                compose_file += `\n    container_name: ${name}`;
+                compose_file += `\n    image: ${image}`;
+
+            // Command
+            if (command_check == 'on') {
+                compose_file += `\n    command: ${command}`
+            }
+
+            // Network mode
+            if (net_mode == 'host') {
+                compose_file += `\n    network_mode: 'host'`
+            }
+            else if (net_mode != 'host' && net_mode != 'docker') {
+                compose_file += `\n    network_mode: '${net_mode}'`
+            }
+            
+            // Restart policy
+            if (restart_policy != '') {
+                compose_file += `\n    restart: ${restart_policy}`
+            }
+
+            // Ports
+            for (let i = 0; i < ports.length; i++) {
+                if ((ports[i] == 'on') && (net_mode != 'host')) {
+                    compose_file += `\n    ports:`
+                    break;
+                }
+            }
+            for (let i = 0; i < ports.length; i++) {
+                if ((ports[i] == 'on') && (net_mode != 'host')) {
+                    compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
+                }
+            }
+
+            // Volumes
+            if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
+                compose_file += `\n    volumes:`
+
+                for (let i = 0; i < 6; i++) {
+
+                    // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
+                    if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
+                        compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
+                    }
+
+                    // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
+                    else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
+                        let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
+                        compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
+                        docker_volumes.push(`${name}_${volume_name}`);
+                    } 
+                }
+            }
+
+            // Environment variables
+            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
+                compose_file += `\n    environment:`
+            }
+            for (let i = 0; i < 12; i++) {
+                if (data[`env${i}`] == 'on') {
+                    compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
+
+                }
+            }
+
+            // Add labels
+            if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
+                compose_file += `\n    labels:`
+            }   
+            for (let i = 0; i < 12; i++) {
+                if (data[`label${i}`] == 'on') {
+                    compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
+                }
+            }
+
+            // Add privileged mode 
+
+            if (data.privileged == 'on') {
+                compose_file += `\n    privileged: true`
+            }
+
+
+            // Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
+            if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
+                for (let i = 0; i < 12; i++) {
+                    if (data[`env${i}`] == 'on') {
+                        if (data[`env_${i}_name`] == 'DRINODE') {
+                            compose_file += `\n    deploy:`
+                            compose_file += `\n      resources:`
+                            compose_file += `\n        reservations:`
+                            compose_file += `\n          devices:`
+                            compose_file += `\n          - driver: nvidia`
+                            compose_file += `\n            count: 1`
+                            compose_file += `\n            capabilities: [gpu]`
+                        }
+                    }
+                }
+            }
 
 
-    res.redirect('/');
+    
+            // add any docker volumes to the docker-compose file
+            if ( docker_volumes.length > 0 ) {
+                compose_file += `\n`
+                compose_file += `\nvolumes:`
 
 
-}
+                // check docker_volumes for duplicates and remove them completely
+                docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
 
 
+                for (let i = 0; i < docker_volumes.length; i++) {
+                    if ( docker_volumes[i] != '') {
+                        compose_file += `\n  ${docker_volumes[i]}:`
+                    }
+                }
+            }
 
 
+            try {   
+                mkdirSync(`./appdata/${name}`, { recursive: true });
+                writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
+
+            } catch { console.log('error creating directory or compose file') }
+
+            try {
+                var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
+
+                (async () => {
+                await compose.pull();
+                await compose.up();
+                })();
+
+            } catch { console.log('error running compose file')}
+
+        }
+
+
+    res.redirect('/');
+
+}
 
 
-// module.exports.install = async function (data) {
-    
-//         console.log(`[Start of install function]`);
-
-//         let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;        
-//         let { port0, port1, port2, port3, port4, port5 } = data;
-//         let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
-//         let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
-//         let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
-
-//         let docker_volumes = [];
-
-//         if (image.startsWith('https://')){
-//             mkdirSync(`./appdata/${name}`, { recursive: true });
-//             execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
-//             console.log(`Downloaded stackfile: ${image}`);
-//             let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
-//             let services = Object.keys(stackfile.services);
-
-//             for ( let i = 0; i < services.length; i++ ) {
-//                 try {
-//                     console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
-//                 } catch { console.log('no env') }
-//             }
-            
-//         } else {
-
-//             let compose_file = `version: '3'`;
-//                 compose_file += `\nservices:`
-//                 compose_file += `\n  ${service_name}:`
-//                 compose_file += `\n    container_name: ${name}`;
-//                 compose_file += `\n    image: ${image}`;
-
-//             // Command
-//             if (command_check == 'on') {
-//                 compose_file += `\n    command: ${command}`
-//             }
-
-//             // Network mode
-//             if (net_mode == 'host') {
-//                 compose_file += `\n    network_mode: 'host'`
-//             }
-//             else if (net_mode != 'host' && net_mode != 'docker') {
-//                 compose_file += `\n    network_mode: '${net_mode}'`
-//             }
-            
-//             // Restart policy
-//             if (restart_policy != '') {
-//                 compose_file += `\n    restart: ${restart_policy}`
-//             }
-
-//             // Ports
-//             if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
-//                 compose_file += `\n    ports:`
-
-//                     for (let i = 0; i < 6; i++) {
-//                         if (data[`port${i}`] == 'on') {
-//                             compose_file += `\n      - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
-//                         }
-//                     }
-//             }
-
-//             // Volumes
-//             if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
-//                 compose_file += `\n    volumes:`
-
-//                 for (let i = 0; i < 6; i++) {
-
-//                     // if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config  )
-//                     if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
-//                         compose_file += `\n      - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-//                     }
-
-//                     // if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
-//                     else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
-//                         let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
-//                         compose_file += `\n      - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
-//                         docker_volumes.push(`${name}_${volume_name}`);
-//                     } 
-//                 }
-//             }
-
-//             // Environment variables
-//             if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-//                 compose_file += `\n    environment:`
-//             }
-//             for (let i = 0; i < 12; i++) {
-//                 if (data[`env${i}`] == 'on') {
-//                     compose_file += `\n      - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
-
-//                 }
-//             }
-
-//             // Add labels
-//             if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
-//                 compose_file += `\n    labels:`
-//             }   
-//             for (let i = 0; i < 12; i++) {
-//                 if (data[`label${i}`] == 'on') {
-//                     compose_file += `\n      - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
-//                 }
-//             }
-
-//             // Add privileged mode 
-
-//             if (data.privileged == 'on') {
-//                 compose_file += `\n    privileged: true`
-//             }
-
-
-//             // Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
-//             if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
-//                 for (let i = 0; i < 12; i++) {
-//                     if (data[`env${i}`] == 'on') {
-//                         if (data[`env_${i}_name`] == 'DRINODE') {
-//                             compose_file += `\n    deploy:`
-//                             compose_file += `\n      resources:`
-//                             compose_file += `\n        reservations:`
-//                             compose_file += `\n          devices:`
-//                             compose_file += `\n          - driver: nvidia`
-//                             compose_file += `\n            count: 1`
-//                             compose_file += `\n            capabilities: [gpu]`
-//                         }
-//                     }
-//                 }
-//             }
 
 
-    
-//             // add any docker volumes to the docker-compose file
-//             if ( docker_volumes.length > 0 ) {
-//                 compose_file += `\n`
-//                 compose_file += `\nvolumes:`
-
-//                 // check docker_volumes for duplicates and remove them completely
-//                 docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
-
-//                 for (let i = 0; i < docker_volumes.length; i++) {
-//                     if ( docker_volumes[i] != '') {
-//                         compose_file += `\n  ${docker_volumes[i]}:`
-//                     }
-//                 }
-//             }
-
-//             try {   
-//                 mkdirSync(`./appdata/${name}`, { recursive: true });
-//                 writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
-
-//             } catch { console.log('error creating directory or compose file') }
-
-//             try {
-//                 var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
-
-//                 (async () => {
-//                 await compose.pull();
-//                 await compose.up();
-//                 })();
-
-//             } catch { console.log('error running compose file')}
-
-//         }
-
-
-// }
-
-
-
-// module.exports.uninstall = async function (data) {
-//     if (data.confirm == 'Yes') {
-//         console.log(`Uninstalling ${data.service_name}: ${data}`);
-//         var containerName = docker.getContainer(`${data.service_name}`);
-//         try {
-//             await containerName.stop();
-//             console.log(`Stopped ${data.service_name} container`);
-//         } catch {
-//             console.log(`Error stopping ${data.service_name} container`);
-//         }
-//         try {
-//             await containerName.remove();
-//             console.log(`Removed ${data.service_name} container`);
-//         } catch {
-//             console.log(`Error removing ${data.service_name} container`);
-//         }
-//     }
-// }

+ 19 - 21
functions/uninstall.js

@@ -1,28 +1,26 @@
-import { writeFileSync, mkdirSync, readFileSync } from "fs";
-import yaml from 'js-yaml';
-import { execSync } from "child_process";
 import { docker } from "../app.js";
 import { docker } from "../app.js";
-import DockerodeCompose from "dockerode-compose";
-
 
 
 
 
 export const Uninstall = async (req, res) => {
 export const Uninstall = async (req, res) => {
 
 
-    console.log('Uninstall')
-    console.log(req.body);
-
-    res.render("/apps", {
-        name: req.session.user,
-        role: req.session.role,
-        avatar: req.session.avatar,
-        list_start: 0,
-        list_end: 28,
-        app_count: 0,
-        prev: 0,
-        next: 0,
-        apps_list: 0,
-    });
-
+    let { confirm, service_name } = req.body;
+
+    if (confirm == 'Yes') {
+        console.log(`Uninstalling ${service_name}`);
+        var containerName = docker.getContainer(`${service_name}`);
+        try {
+            await containerName.stop();
+            console.log(`Stopped ${service_name} container`);
+        } catch {
+            console.log(`Error stopping ${service_name} container`);
+        }
+        try {
+            await containerName.remove();
+            console.log(`Removed ${service_name} container`);
+        } catch {
+            console.log(`Error removing ${service_name} container`);
+        }
+    }
+    res.redirect('/');
 }
 }
 
 
-

+ 1 - 2
router/index.js

@@ -1,4 +1,5 @@
 import express from "express";
 import express from "express";
+export const router = express.Router();
 
 
 // Controllers
 // Controllers
 import { Login, submitLogin, Logout } from "../controllers/login.js";
 import { Login, submitLogin, Logout } from "../controllers/login.js";
@@ -14,8 +15,6 @@ import { Volumes } from "../controllers/volumes.js";
 import { Syslogs } from "../controllers/syslogs.js";
 import { Syslogs } from "../controllers/syslogs.js";
 import { Portal } from "../controllers/portal.js"
 import { Portal } from "../controllers/portal.js"
 
 
-export const router = express.Router();
-
 /// Functions
 /// Functions
 import { Install } from "../functions/install.js"
 import { Install } from "../functions/install.js"
 import { Uninstall } from "../functions/uninstall.js"
 import { Uninstall } from "../functions/uninstall.js"

+ 0 - 82
views/dashboard.ejs

@@ -126,88 +126,6 @@
 
 
             <!-- containerCards get inserted here from public/js/main.js -->
             <!-- containerCards get inserted here from public/js/main.js -->
             
             
-            
-            <div class="modal modal-blur fade" id="remove_modal" tabindex="-1" style="display: none;" aria-hidden="true">
-              <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
-                <div class="modal-content">
-                  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-                  <div class="modal-status bg-danger"></div>
-                  <div class="modal-body text-center py-3">
-                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
-                    <h3>Remove ${name}?</h3>
-                    <form action="/uninstall" id="${name}_uninstall" method="POST">
-                    <input type="text" class="form-control" name="service_name" value="${name}" hidden/>
-                    <div class="mb-3"> </div>
-                    
-                    <div class="mb-2">
-                      <div class="divide-y">
-                        <div class="row">
-                          <div class="col-9">
-                            <label class="row text-start">
-                              <span class="col">Remove Volumes</span>
-                            </label>
-                          </div>
-                          <div class="col-3">
-                            <label class="form-check form-check-single form-switch text-end">
-                              <input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
-                            </label>
-                          </div>
-                        </div>
-                        <div class="row">
-                          <div class="col-9">
-                            <label class="row text-start">
-                              <span class="col">
-                                Remove Image
-                              </span>
-                            </label>
-                          </div>
-                          <div class="col-3">
-                            <label class="form-check form-check-single form-switch text-end">
-                              <input class="form-check-input" type="checkbox" name="remove_image" disabled="">
-                            </label>
-                          </div>
-                        </div>
-                        <div class="row">
-                          <div class="col-9">
-                            <label class="row text-start">
-                              <span class="col">
-                                Remove Backups
-                              </span>
-                            </label>
-                          </div>
-                          <div class="col-3">
-                            <label class="form-check form-check-single form-switch text-end">
-                              <input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
-                            </label>
-                          </div>
-                        </div>
-                      </div>
-                    </div>
-                    <div class="mt-1"> </div>
-                    <div class="text-muted">Enter "Yes" below to remove the container.</div>
-                    <input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
-                    </form>
-                  </div>
-                  <div class="modal-footer">
-                    <div class="w-100">
-                      <div class="row">
-                        <div class="col">
-                          <a href="#" class="btn w-100" data-bs-dismiss="modal">
-                            Cancel
-                          </a>
-                        </div>
-                        <div class="col">
-                          <input type="submit" form="${name}_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
-                        </div>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </div>
-            
-        
-            
             <div class="modal modal-blur fade" id="details_modal" tabindex="-1" role="dialog" aria-hidden="true">
             <div class="modal modal-blur fade" id="details_modal" tabindex="-1" role="dialog" aria-hidden="true">
                         <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
                         <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
                           <div class="modal-content">
                           <div class="modal-content">