Merge pull request #9 from lllllllillllllillll/dev
Merge dev into main for v0.04
This commit is contained in:
commit
39bb0485b9
16 changed files with 3038 additions and 138 deletions
|
@ -1,3 +1,9 @@
|
|||
## v0.04 (Nov 11th 2023)
|
||||
* Docker Image and Compose file available.
|
||||
* The containers DweebUI and DweebCache are hidden from the dashboard.
|
||||
* Default icon for containers.
|
||||
* Fixed missing information in container details/edit modals (Ports, Env, Volumes, Labels).
|
||||
|
||||
## v0.03 (Nov 5th 2023)
|
||||
* Container graphs now load instantly on refresh
|
||||
* Working net data for server dashboard
|
||||
|
|
36
Dockerfile
Normal file
36
Dockerfile
Normal file
|
@ -0,0 +1,36 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Dockerfile reference guide at
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
ARG NODE_VERSION=20.0.0
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine
|
||||
|
||||
# Use production node environment by default.
|
||||
ENV NODE_ENV production
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.npm to speed up subsequent builds.
|
||||
# Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into
|
||||
# into this layer.
|
||||
RUN --mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm ci --omit=dev
|
||||
|
||||
# Run the application as a non-root user.
|
||||
USER root
|
||||
|
||||
# Copy the rest of the source files into the image.
|
||||
COPY . .
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application.
|
||||
CMD node app.js
|
37
README.md
37
README.md
|
@ -3,13 +3,13 @@
|
|||
|
||||
DweebUI is a simple Docker web interface created with javascript and node.js
|
||||
|
||||
Pre-Pre-Pre-Pre-Pre Alpha v 0.03 ( :fire: Experimental. Don't install on any servers you care about :fire: )
|
||||
Pre-Pre-Pre-Pre-Pre Alpha v 0.04 ( :fire: Experimental. Don't install on any servers you care about :fire: )
|
||||
|
||||
* I haven't used Github very much and I'm still new to javascript
|
||||
* I haven't used Github very much and I'm still new to javascript.
|
||||
* This is the first project I've ever released and I'm sure it's full of plenty of bugs and mistakes.
|
||||
* I probably should have waited a lot longer to share this :|
|
||||
|
||||
Requirements: Fresh Install of Debian 12.2
|
||||
Requirements: Docker
|
||||
|
||||

|
||||
|
||||
|
@ -26,8 +26,37 @@ Requirements: Fresh Install of Debian 12.2
|
|||
|
||||
## Setup
|
||||
|
||||
* Download and extract DweebUI.zip to a fresh Debian 12.2 Install
|
||||
* Docker compose.yaml:
|
||||
```
|
||||
services:
|
||||
dweebui:
|
||||
container_name: DweebUI
|
||||
image: lllllllillllllillll/dweebui:v0.04
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
- cache
|
||||
links:
|
||||
- cache
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
cache:
|
||||
container_name: DweebCache
|
||||
image: redis:6.2-alpine
|
||||
restart: always
|
||||
command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
|
||||
volumes:
|
||||
- cache:/data
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
cache:
|
||||
```
|
||||
|
||||
* Using setup.sh:
|
||||
```
|
||||
Extract DweebUI.zip and navigate to /DweebUI
|
||||
cd DweebUI
|
||||
chmod +x setup.sh
|
||||
sudo ./setup.sh
|
||||
|
|
5
app.js
5
app.js
|
@ -9,9 +9,8 @@ const { serverStats, containerList, containerStats, containerAction } = require(
|
|||
let sent_list, clicked;
|
||||
|
||||
const redisClient = require('redis').createClient({
|
||||
host:'localhost',
|
||||
port:6379,
|
||||
password:'somesupersecretpassword',
|
||||
url: 'redis://DweebCache:6379',
|
||||
password:'eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81',
|
||||
legacyMode:true
|
||||
});
|
||||
redisClient.connect().catch(console.log);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module.exports.dashCard = function dashCard(data) {
|
||||
|
||||
let { name, service, id, state, image, external_port, internal_port } = data;
|
||||
let { name, service, id, state, image, external_port, internal_port, ports, volumes, environment_variables, labels } = data;
|
||||
|
||||
//disable controls for a docker container depending on its name
|
||||
let enabled = "";
|
||||
|
@ -32,77 +32,90 @@ module.exports.dashCard = function dashCard(data) {
|
|||
|
||||
let restart_policy = 'unless-stopped';
|
||||
|
||||
|
||||
let ports_data = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (ports) {
|
||||
ports_data = ports;
|
||||
} else {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
let port_check = "checked";
|
||||
let external = i;
|
||||
let internal = i;
|
||||
let protocol = "tcp";
|
||||
let port_check = "checked";
|
||||
let external = i;
|
||||
let internal = i;
|
||||
let protocol = "tcp";
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: external,
|
||||
internal: internal,
|
||||
protocol: protocol
|
||||
});
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: external,
|
||||
internal: internal,
|
||||
protocol: protocol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let volumes_data = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (volumes) {
|
||||
volumes_data = volumes;
|
||||
} else {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
let vol_check = "checked";
|
||||
let bind = i;
|
||||
let container = i;
|
||||
let readwrite = "rw";
|
||||
let vol_check = "checked";
|
||||
let bind = i;
|
||||
let container = i;
|
||||
let readwrite = "rw";
|
||||
|
||||
volumes_data.push({
|
||||
check: vol_check,
|
||||
bind: bind,
|
||||
container: container,
|
||||
readwrite: readwrite
|
||||
});
|
||||
volumes_data.push({
|
||||
check: vol_check,
|
||||
bind: bind,
|
||||
container: container,
|
||||
readwrite: readwrite
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let env_data = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (environment_variables) {
|
||||
env_data = environment_variables;
|
||||
} else {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
let env_check = "checked";
|
||||
let env_name = i;
|
||||
let env_default = i;
|
||||
let env_check = "checked";
|
||||
let env_name = i;
|
||||
let env_default = i;
|
||||
|
||||
env_data.push({
|
||||
env_check: env_check,
|
||||
env_name: env_name,
|
||||
env_default: env_default
|
||||
});
|
||||
env_data.push({
|
||||
check: env_check,
|
||||
name: env_name,
|
||||
default: env_default
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let label_data = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (labels) {
|
||||
label_data = labels;
|
||||
} else {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
let label_check = "checked";
|
||||
let label_name = i;
|
||||
let label_default = i;
|
||||
|
||||
label_data.push({
|
||||
label_check: label_check,
|
||||
label_name: label_name,
|
||||
label_default: label_default
|
||||
});
|
||||
let label_check = "checked";
|
||||
let label_name = i;
|
||||
let label_default = i;
|
||||
|
||||
label_data.push({
|
||||
check: label_check,
|
||||
name: label_name,
|
||||
value: label_default
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return `
|
||||
<div class="col-sm-6 col-lg-3 deleteme">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-stamp card-stamp-sm">
|
||||
<img heigh="150px" width="150px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png"></img>
|
||||
<img heigh="150px" width="150px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/dweebui.png';"></img>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
|
|
27
compose.yaml
Normal file
27
compose.yaml
Normal file
|
@ -0,0 +1,27 @@
|
|||
services:
|
||||
dweebui:
|
||||
container_name: DweebUI
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
- cache
|
||||
links:
|
||||
- cache
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
cache:
|
||||
container_name: DweebCache
|
||||
image: redis:6.2-alpine
|
||||
restart: always
|
||||
command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
|
||||
volumes:
|
||||
- cache:/data
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
cache:
|
|
@ -1,11 +1,10 @@
|
|||
const User = require('../database/UserModel');
|
||||
const { appCard } = require('../components/appCard')
|
||||
const { exec, execSync } = require("child_process");
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const { install } = require('../functions/package_manager');
|
||||
const { install, uninstall } = require('../functions/package_manager');
|
||||
|
||||
// import { install, uninstall } from '../functions/package_manager';
|
||||
|
||||
const templates_json = require('../templates.json');
|
||||
let templates = templates_json.templates;
|
||||
|
@ -29,7 +28,6 @@ exports.Apps = async function(req, res) {
|
|||
let list_end = (page * 28);
|
||||
let last_page = Math.ceil(templates.length / 28);
|
||||
|
||||
// generate values for prev and next buttons so that i can go back and forth between pages
|
||||
let prev = '/apps?page=' + (page - 1);
|
||||
let next = '/apps?page=' + (page + 1);
|
||||
if (page == 1) {
|
||||
|
@ -78,7 +76,6 @@ exports.searchApps = async function(req, res) {
|
|||
let list_end = (page * 28);
|
||||
let last_page = Math.ceil(templates.length / 28);
|
||||
|
||||
// generate values for prev and next buttons so that i can go back and forth between pages
|
||||
let prev = '/apps?page=' + (page - 1);
|
||||
let next = '/apps?page=' + (page + 1);
|
||||
if (page == 1) {
|
||||
|
@ -95,7 +92,6 @@ exports.searchApps = async function(req, res) {
|
|||
|
||||
// split value of search into an array of words
|
||||
search = search.split(' ');
|
||||
|
||||
try {console.log(search[0]);} catch (error) {}
|
||||
try {console.log(search[1]);} catch (error) {}
|
||||
try {console.log(search[2]);} catch (error) {}
|
||||
|
@ -179,11 +175,9 @@ exports.Uninstall = async function (req, res) {
|
|||
|
||||
|
||||
if (req.body.confirm == 'Yes') {
|
||||
exec(`docker compose -f ./appdata/${req.body.service_name}/docker-compose.yml down`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
|
||||
uninstall(req.body);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -51,8 +51,6 @@ exports.processRegister = async function(req,res){
|
|||
avatar: `<img src="./static/avatars/${avatar}">`
|
||||
});
|
||||
|
||||
console.log(`Created: ${user.first_name}`);
|
||||
|
||||
// set the session.
|
||||
req.session.user = user.username;
|
||||
req.session.UUID = user.UUID;
|
||||
|
|
|
@ -2,7 +2,7 @@ const { Sequelize, DataTypes } = require('sequelize');
|
|||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: 'database/db.sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ const { Sequelize, DataTypes } = require('sequelize');
|
|||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: 'database/db.sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
|
||||
const { exec, execSync } = require("child_process");
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const { exec, execSync } = require("child_process");
|
||||
|
||||
const { docker } = require('./system_information');
|
||||
var DockerodeCompose = require('dockerode-compose');
|
||||
|
||||
|
||||
module.exports.install = async function (data) {
|
||||
|
@ -124,13 +126,18 @@ module.exports.install = async function (data) {
|
|||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
exec(`docker compose -f ./appdata/${name}/docker-compose.yml up -d`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
} 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')}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -140,22 +147,25 @@ module.exports.install = async function (data) {
|
|||
|
||||
module.exports.uninstall = async function (data) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
if (data.confirm == 'Yes') {
|
||||
exec(`docker compose -f ./appdata/${data.service_name}/docker-compose.yml down`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
|
||||
|
||||
var containerName = docker.getContainer(`${data.service_name}`);
|
||||
|
||||
try {
|
||||
containerName.stop(function (err, data) {
|
||||
});
|
||||
} catch { console.log('unable to stop container') }
|
||||
|
||||
|
||||
try {
|
||||
containerName.remove(function (err, data) {
|
||||
});
|
||||
} catch { console.log('unable to remove container') }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,8 @@ var Docker = require('dockerode');
|
|||
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
|
||||
|
||||
// export docker
|
||||
module.exports.docker = docker;
|
||||
|
||||
module.exports.serverStats = async function () {
|
||||
const cpuUsage = await currentLoad();
|
||||
|
@ -29,55 +30,128 @@ module.exports.containerList = async function () {
|
|||
|
||||
const data = await docker.listContainers({ all: true });
|
||||
for (const container of data) {
|
||||
let imageVersion = container.Image.split('/');
|
||||
let service = imageVersion[imageVersion.length - 1].split(':')[0];
|
||||
|
||||
let containerId = docker.getContainer(container.Id);
|
||||
let containerInfo = await containerId.inspect();
|
||||
|
||||
let open_ports = [];
|
||||
let external_port = 0;
|
||||
let internal_port = 0;
|
||||
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
|
||||
|
||||
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
|
||||
open_ports.push(`${value[0].HostPort}`);
|
||||
external_port = value[0].HostPort;
|
||||
internal_port = key;
|
||||
let imageVersion = container.Image.split('/');
|
||||
let service = imageVersion[imageVersion.length - 1].split(':')[0];
|
||||
|
||||
if ((external_port == undefined) || (internal_port == undefined)) {
|
||||
external_port = 0;
|
||||
internal_port = 0;
|
||||
let containerId = docker.getContainer(container.Id);
|
||||
let containerInfo = await containerId.inspect();
|
||||
|
||||
let external_port = 0;
|
||||
let internal_port = 0;
|
||||
|
||||
// Get ports
|
||||
let ports_list = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check : 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (ports_list[i] == undefined) {
|
||||
let ports = {
|
||||
check : '',
|
||||
external: '',
|
||||
internal: '',
|
||||
protocol: ''
|
||||
}
|
||||
ports_list[i] = ports;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get volumes.
|
||||
let volumes_list = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
|
||||
let volumes = {
|
||||
check : 'checked',
|
||||
bind: value.split(':')[0],
|
||||
container: value.split(':')[1],
|
||||
readwrite: value.split(':')[2]
|
||||
}
|
||||
volumes_list.push(volumes);
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (volumes_list[i] == undefined) {
|
||||
let volumes = {
|
||||
check : '',
|
||||
bind: '',
|
||||
container: '',
|
||||
readwrite: ''
|
||||
}
|
||||
volumes_list[i] = volumes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get environment variables.
|
||||
let environment_variables = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
|
||||
let env = {
|
||||
check : 'checked',
|
||||
name: value.split('=')[0],
|
||||
default: value.split('=')[1]
|
||||
}
|
||||
environment_variables.push(env);
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (environment_variables[i] == undefined) {
|
||||
let env = {
|
||||
check : '',
|
||||
name: '',
|
||||
default: ''
|
||||
}
|
||||
environment_variables[i] = env;
|
||||
}
|
||||
}
|
||||
|
||||
// Get labels.
|
||||
let labels = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
|
||||
let label = {
|
||||
check : 'checked',
|
||||
name: key,
|
||||
value: value
|
||||
}
|
||||
labels.push(label);
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (labels[i] == undefined) {
|
||||
let label = {
|
||||
check : '',
|
||||
name: '',
|
||||
value: ''
|
||||
}
|
||||
labels[i] = label;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let container_info = {
|
||||
name: container.Names[0].slice(1),
|
||||
service: service,
|
||||
id: container.Id,
|
||||
state: container.State,
|
||||
image: container.Image,
|
||||
external_port: external_port,
|
||||
internal_port: internal_port,
|
||||
ports: ports_list,
|
||||
volumes: volumes_list,
|
||||
environment_variables: environment_variables,
|
||||
labels: labels,
|
||||
}
|
||||
|
||||
let dockerCard = dashCard(container_info);
|
||||
|
||||
card_list += dockerCard;
|
||||
}
|
||||
|
||||
let volumes = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Mounts)) {
|
||||
volumes.push(`${value.Source}: ${value.Destination}: ${value.RW}`);
|
||||
}
|
||||
|
||||
let environment_variables = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
|
||||
environment_variables.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
let labels = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
|
||||
labels.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
|
||||
let container_info = {
|
||||
name: container.Names[0].slice(1),
|
||||
service: service,
|
||||
id: container.Id,
|
||||
state: container.State,
|
||||
image: container.Image,
|
||||
external_port: external_port,
|
||||
internal_port: internal_port
|
||||
}
|
||||
|
||||
let dockerCard = dashCard(container_info);
|
||||
card_list += dockerCard;
|
||||
|
||||
}
|
||||
|
||||
|
|
2713
package-lock.json
generated
Normal file
2713
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -10,15 +10,16 @@
|
|||
"child_process": "^1.0.2",
|
||||
"connect-redis": "^6.1.3",
|
||||
"dockerode": "^3.3.5",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"redis": "^4.6.5",
|
||||
"sequelize": "^6.32.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"systeminformation": "^5.17.12",
|
||||
"js-yaml": "^4.1.0"
|
||||
"systeminformation": "^5.17.12"
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#" class="link-secondary" rel="noopener">
|
||||
v0.03
|
||||
v0.04
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<div class="navbar-nav flex-row order-md-last">
|
||||
<div class="nav-item d-none d-md-flex me-3">
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn text-black">
|
||||
<a href="#" class="btn text-green">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/lock -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" 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="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path> <path d="M8 11v-4a4 4 0 1 1 8 0v4"></path> </svg>
|
||||
VPN
|
||||
|
@ -69,7 +69,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-shield" 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 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"></path> </svg>
|
||||
Firewall
|
||||
</a>
|
||||
<a href="#" class="btn text-black">
|
||||
<a href="#" class="btn text-green">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/shield -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-screen-share" 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="M21 12v3a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10a1 1 0 0 1 1 -1h9"></path> <path d="M7 20l10 0"></path> <path d="M9 16l0 4"></path> <path d="M15 16l0 4"></path> <path d="M17 4h4v4"></path> <path d="M16 9l5 -5"></path> </svg>
|
||||
VNC
|
||||
|
|
Loading…
Add table
Reference in a new issue