Compare commits
52 Commits
main
...
14079b25b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 14079b25b7 | |||
| 5bc57ba497 | |||
| 0991d1a013 | |||
| 9c5d8f3bdc | |||
| 5842a16c9b | |||
| 2c8ef59c19 | |||
| 6678cfda7d | |||
| c8f828bfb0 | |||
| 195cc47368 | |||
| eb368f9860 | |||
| d26aa8c9cb | |||
| 471b13739d | |||
| d0e107f644 | |||
| 630ceb2473 | |||
| d233c8081a | |||
| bbd8c1e40c | |||
| 57fb2febf6 | |||
| a5b9823b10 | |||
| a2a4fdd770 | |||
| 9034c23d84 | |||
| 685fe225f4 | |||
| 0e8a99a277 | |||
| 76ac71a755 | |||
| 7251753df5 | |||
| 75355c43a8 | |||
| 7d1f536af5 | |||
| 8c79806318 | |||
| db03c8a375 | |||
| a698a88983 | |||
| 31320f3796 | |||
| b640426064 | |||
| f307bcb79b | |||
| 1e88ddc50d | |||
| 5905f8f810 | |||
| 6af6def556 | |||
| ed742d9ab8 | |||
| 9f55fe687d | |||
| 0fbade138d | |||
| e717f85c52 | |||
| d45693bc9b | |||
| 93ab8ff1c3 | |||
| 66f1603eeb | |||
| 6fa1beac99 | |||
| c94a2bf5d9 | |||
| 21581bd9e5 | |||
| ae7ccc9081 | |||
| 6e398f72da | |||
| a105fc44a8 | |||
| 2c1ad22bf7 | |||
| 81ddc796c2 | |||
| 6d4da254cd | |||
| ea9cdb7692 |
57
.drone.yml
@@ -33,12 +33,13 @@ platform:
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: Publish to ghcr
|
||||
- name: Publish app to ghcr
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: ghcr.io
|
||||
repo: ghcr.io/kevinmidboe/${DRONE_REPO_NAME}
|
||||
dockerfile: Dockerfile
|
||||
compress: true
|
||||
username:
|
||||
from_secret: GITHUB_USERNAME
|
||||
password:
|
||||
@@ -55,6 +56,7 @@ trigger:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
- update
|
||||
|
||||
depends_on:
|
||||
- Build
|
||||
@@ -79,6 +81,7 @@ steps:
|
||||
commands:
|
||||
- mkdir -p /root/.kube
|
||||
- echo "IMAGE=ghcr.io/kevinmidboe/${DRONE_REPO_NAME}:${DRONE_COMMIT_SHA}" > /root/.kube/.env
|
||||
- echo "VARNISH_IMAGE=ghcr.io/kevinmidboe/varnish-${DRONE_REPO_NAME}:latest" >> /root/.kube/.env
|
||||
- echo "NAMESPACE=${DRONE_REPO_NAME}" >> /root/.kube/.env
|
||||
- 'curl -s
|
||||
-H "X-Vault-Token: $VAULT_TOKEN"
|
||||
@@ -117,6 +120,7 @@ trigger:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
- update
|
||||
|
||||
depends_on:
|
||||
- Build
|
||||
@@ -125,8 +129,57 @@ depends_on:
|
||||
volumes:
|
||||
- name: kube-config
|
||||
temp: {}
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: Publish varnish
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: Check for varnish changes
|
||||
image: alpine/git
|
||||
commands:
|
||||
- git fetch --no-tags --depth=2
|
||||
- |
|
||||
if git diff-tree --no-commit-id --name-only -r HEAD | grep -qE '(\.drone.yml|(varnish/.+(vcl|tmpl)(\n|$)))'; then
|
||||
echo "Changes detected in varnish config"
|
||||
else
|
||||
echo "No changes in varnish config file, skipping..."
|
||||
exit 78 # exit code 78 = skip in Drone
|
||||
fi
|
||||
|
||||
- name: Publish varnish image to ghcr
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: ghcr.io
|
||||
repo: ghcr.io/kevinmidboe/varnish-infra-map
|
||||
context: varnish
|
||||
dockerfile: varnish/Dockerfile
|
||||
compress: true
|
||||
username:
|
||||
from_secret: GITHUB_USERNAME
|
||||
password:
|
||||
from_secret: GHCR_UPLOAD_TOKEN
|
||||
tags:
|
||||
- latest
|
||||
- ${DRONE_COMMIT_SHA}
|
||||
|
||||
trigger:
|
||||
event:
|
||||
include:
|
||||
- push
|
||||
exclude:
|
||||
- pull_request
|
||||
branch:
|
||||
- main
|
||||
- update
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: bea117f5e4b51c4fc215ae86962a8cfb24993d9e9b7db3498ab5940b10c70d69
|
||||
hmac: b4b6a98b76fdf3cf297b46cf986a3d46f3d4050e623f2c769267181c7075a6ca
|
||||
|
||||
...
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
DATABASE_URL=
|
||||
TRAEFIK_URL=
|
||||
HTTP_HEALTH_ENDPOINTS=
|
||||
PROXMOX_URL=
|
||||
PROXMOX_TOKEN=
|
||||
HOMEASSISTANT_URL=
|
||||
HOMEASSISTANT_TOKEN=
|
||||
TRAEFIK_URL=
|
||||
KUBERNETES_SERVICE_HOST=
|
||||
KUBERNETES_SA_TOKEN=
|
||||
KUBERNETES_CA_CERT_PATH=
|
||||
|
||||
|
||||
@@ -11,3 +11,7 @@ data:
|
||||
HOMEASSISTANT_URL: ${HOMEASSISTANT_URL}
|
||||
HOMEASSISTANT_TOKEN: ${HOMEASSISTANT_TOKEN}
|
||||
TRAEFIK_URL: ${TRAEFIK_URL}
|
||||
HTTP_HEALTH_ENDPOINTS: ${HTTP_HEALTH_ENDPOINTS}
|
||||
KUBERNETES_SERVICE_HOST: ${KUBERNETES_SERVICE_HOST}
|
||||
KUBERNETES_SA_TOKEN: ${KUBERNETES_SA_TOKEN}
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
|
||||
9
.kubernetes/2-config-varnish.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: varnish-config
|
||||
namespace: ${NAMESPACE}
|
||||
data:
|
||||
PROXY_HOST: ${PROXY_HOST}
|
||||
IMAGE_HOST: ${IMAGE_HOST}
|
||||
46
.kubernetes/deployment-app.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
labels:
|
||||
app: infra-map
|
||||
name: infra-map
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: infra-map
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: infra-map
|
||||
spec:
|
||||
containers:
|
||||
- name: infra-map
|
||||
image: ${IMAGE}
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 828Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
env:
|
||||
- name: ORIGIN
|
||||
value: http://infra-map.infra-map.svc.cluster.local:3000
|
||||
- name: PROTOCOL_HEADER
|
||||
value: x-forwarded-proto
|
||||
- name: HOST_HEADER
|
||||
value: x-forwarded-host
|
||||
- name: PORT_HEADER
|
||||
value: x-forwarded-port
|
||||
- name: ENV
|
||||
value: production
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: secret-env-values
|
||||
imagePullSecrets:
|
||||
- name: ghcr-login-secret
|
||||
40
.kubernetes/deployment-varnish.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
labels:
|
||||
app: varnish
|
||||
name: varnish
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: varnish
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: varnish
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- /usr/local/bin/docker-entrypoint.sh
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: varnish-config
|
||||
image: ghcr.io/kevinmidboe/varnish-infra-map:latest
|
||||
imagePullPolicy: Always
|
||||
name: varnish
|
||||
resources:
|
||||
limits:
|
||||
cpu: 900m
|
||||
memory: 828Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
imagePullSecrets:
|
||||
- name: ghcr-login-secret
|
||||
dnsPolicy: ClusterFirst
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
labels:
|
||||
app: infra-map
|
||||
name: infra-map
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: infra-map
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: infra-map
|
||||
spec:
|
||||
containers:
|
||||
- image: ${IMAGE}
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: infra-map
|
||||
env:
|
||||
- name: HOMEASSISTANT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: HOMEASSISTANT_TOKEN
|
||||
- name: HOMEASSISTANT_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: HOMEASSISTANT_URL
|
||||
- name: PROXMOX_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: PROXMOX_TOKEN
|
||||
- name: PROXMOX_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: PROXMOX_URL
|
||||
- name: TRAEFIK_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: secret-env-values
|
||||
key: TRAEFIK_URL
|
||||
resources:
|
||||
limits:
|
||||
cpu: 900m
|
||||
memory: 828Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
restartPolicy: Always
|
||||
imagePullSecrets:
|
||||
- name: ghcr-login-secret
|
||||
@@ -12,7 +12,7 @@ spec:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: infra-map-service
|
||||
name: varnish
|
||||
port:
|
||||
number: 80
|
||||
path: /
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: varnish
|
||||
name: varnish
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
name: http-varnish
|
||||
protocol: TCP
|
||||
targetPort: 6081
|
||||
selector:
|
||||
app: varnish
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: infra-map
|
||||
name: infra-map-service
|
||||
name: infra-map
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
name: http-app
|
||||
protocol: TCP
|
||||
targetPort: 3000
|
||||
selector:
|
||||
app: infra-map
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
status:
|
||||
loadBalancer: {}
|
||||
|
||||
23
Dockerfile
@@ -1,23 +1,34 @@
|
||||
# --- Stage 1: Compile svelte-kit project ---
|
||||
FROM node:22-alpine3.20 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source files
|
||||
COPY src/ src
|
||||
COPY static/ static
|
||||
COPY package.json yarn.lock svelte.config.js tsconfig.json vite.config.ts ./
|
||||
|
||||
RUN yarn
|
||||
# Install dependencies
|
||||
RUN yarn --frozen-lockfile
|
||||
|
||||
# Build project
|
||||
ENV NODE_ENV=production
|
||||
RUN yarn build
|
||||
|
||||
# --- Stage 2: Run project with node ---
|
||||
FROM node:22-alpine3.20
|
||||
|
||||
# Copy compiled project files
|
||||
WORKDIR /opt/infra-map
|
||||
COPY --from=builder /app/build build
|
||||
|
||||
COPY package.json .
|
||||
RUN yarn --production
|
||||
|
||||
EXPOSE 3000
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD [ "node", "build" ]
|
||||
# Install dependencies
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn add @sveltejs/kit
|
||||
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
|
||||
CMD [ "node", "build/index.js" ]
|
||||
|
||||
106
README.md
@@ -1,38 +1,110 @@
|
||||
# sv
|
||||
# infra-map
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
## Configuration setup
|
||||
|
||||
## Creating a project
|
||||
set the following env variables during runtime or update `.env` file.
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
### Kubernetes
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
KUBERNETES_SERVICE_HOST=https://IP_ADDRESS:16443
|
||||
KUBERNETES_CA_CART_PATH=kube-ca.crt
|
||||
KUBERNETES_SA_TOKEN=LKdgk34l...
|
||||
```
|
||||
|
||||
The `KUBERNETES_SERVICE_HOST` is the api server, [microk8s documentation](https://microk8s.io/docs/services-and-ports#services-binding-to-the-default-host-interface) describes that API server is running at port `16443`. Runtime in a pod this will be set by kubernetes.
|
||||
|
||||
Get ca_cert from the any running pod at: `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`.
|
||||
|
||||
### Proxmox
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
PROXMOX_URL=https://apollo.schleppe:8086/api2/json/
|
||||
PROXMOX_TOKEN=PVEAPITOKEN=USER@pve!USER=TOKEN_VALUE
|
||||
```
|
||||
|
||||
`PROXMOX_TOKEN` should contain the sub-variable `PVEAPITOKEN` that describes a proxmox API token.
|
||||
|
||||
Create api token:
|
||||
|
||||
- Create Users
|
||||
- user name: infra-map
|
||||
- realm: pve
|
||||
- expire: never
|
||||
- Add API tokens
|
||||
- user: infra-map@pve
|
||||
- token ID: infra-map
|
||||
- Permissions
|
||||
- add: API Token permissions
|
||||
- path: /
|
||||
- api-token: infra-map@pve!infra-map
|
||||
- role: Administrator
|
||||
- propagate: true
|
||||
|
||||
## Home Assistant
|
||||
|
||||
Required parameters:
|
||||
|
||||
```bash
|
||||
HOMEASSISTANT_URL=http://homeassistant.schleppe:8123/api/states
|
||||
HOMEASSISTANT_TOKEN=
|
||||
```
|
||||
|
||||
Follow hass documentation on generating a api token: https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token.
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
To create a production version to be run from node:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
yarn build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
To preview either use vite to serve or execute node entrypoint:
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
```bash
|
||||
yarn preview
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
node build/index.js
|
||||
```
|
||||
|
||||
## Run using docker
|
||||
|
||||
The application is configure to be used either standalone or behind a webserver. To build the svelte-application docker image run:
|
||||
|
||||
> NB! Remember to configure .env, which is automatically picked up by docker-compose. Set and override for both containers in this file.
|
||||
|
||||
Svelte-kit application:
|
||||
|
||||
```bash
|
||||
docker build -t infra-map .
|
||||
```
|
||||
|
||||
Varnish cache:
|
||||
|
||||
```bash
|
||||
cd varnish
|
||||
docker build -t infra-varnish-cache .
|
||||
```
|
||||
|
||||
Or both using docker compose:
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
varnish:
|
||||
build:
|
||||
context: varnish
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
# sets environment variables. Overridden by env, but has sane defaults
|
||||
IMAGE_HOST: ${IMAGE_HOST:-homeassistant.local}
|
||||
PROXY_HOST: ${PROXY_HOST:-app}
|
||||
ports:
|
||||
- '6081:6081'
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
env_file: .env # sets container's environment
|
||||
environment:
|
||||
- ORIGIN=http://localhost:3000
|
||||
- NODE_ENV=production
|
||||
- PROTOCOL_HEADER=x-forwarded-proto
|
||||
- HOST_HEADER=x-forwarded-host
|
||||
- PORT_HEADER=x-forwarded-port
|
||||
25
package.json
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build": "svelte-kit sync && vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -16,24 +16,27 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.27.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||
"@zerodevx/svelte-json-view": "^1.0.11",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"force-graph": "^1.50.1",
|
||||
"pg": "^8.16.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"sass-embedded": "^1.86.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte": "^5.38.2",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"sveltekit-sse": "^0.13.16",
|
||||
"typescript": "^5.9.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"sveltekit-sse": "^0.13.16"
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
53
scripts/icon-converter.js
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Usage: node convert-svg-to-svelte.js [inputDir] [outputDir]
|
||||
* Defaults: ./svgs ./svelte
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const INPUT_DIR = process.argv[2] || '../svgs';
|
||||
const OUTPUT_DIR = process.argv[3] || '../src/lib/icons';
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
function processSvg(svgContent) {
|
||||
// Strip XML/DOCTYPE
|
||||
let out = svgContent.replace(/<\?xml[\s\S]*?\?>\s*/i, '').replace(/<!DOCTYPE[\s\S]*?>\s*/i, '');
|
||||
|
||||
// Remove ALL comments
|
||||
out = out.replace(/<!--[\s\S]*?-->\s*/g, '');
|
||||
|
||||
// Remove <g id="icomoon-ignore"></g> with any whitespace between tags
|
||||
out = out.replace(/<g\s+id=(["'])icomoon-ignore\1\s*>\s*<\/g>\s*/gi, '');
|
||||
|
||||
// Ensure only width="100%" height="100%" on the <svg> tag
|
||||
out = out.replace(/<svg\b[^>]*>/i, (match) => {
|
||||
let tag = match
|
||||
.replace(/\s+(width|height)\s*=\s*"[^"]*"/gi, '')
|
||||
.replace(/\s+(width|height)\s*=\s*'[^']*'/gi, '');
|
||||
return tag.replace(/>$/, ' width="100%" height="100%">');
|
||||
});
|
||||
|
||||
// Prepend the single license comment
|
||||
out = '<!-- generated by icomoon.io - licensed Lindua icon -->\n' + out.replace(/^\s+/, '');
|
||||
return out;
|
||||
}
|
||||
|
||||
function convertSvgs(inputDir = INPUT_DIR, outputDir = OUTPUT_DIR) {
|
||||
if (!fs.existsSync(inputDir)) {
|
||||
console.warn(`Input directory not found: ${inputDir}`);
|
||||
return;
|
||||
}
|
||||
const files = fs.readdirSync(inputDir).filter((f) => f.toLowerCase().endsWith('.svg'));
|
||||
files.forEach((file) => {
|
||||
const src = path.join(inputDir, file);
|
||||
const dest = path.join(outputDir, file.replace(/\.svg$/i, '.svelte'));
|
||||
const svgContent = fs.readFileSync(src, 'utf8');
|
||||
const processed = processSvg(svgContent);
|
||||
fs.writeFileSync(dest, processed, 'utf8');
|
||||
console.log(`Converted: ${file} -> ${path.basename(dest)}`);
|
||||
});
|
||||
}
|
||||
|
||||
convertSvgs();
|
||||
@@ -5,6 +5,14 @@
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#32292B" />
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="infra" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
88
src/lib/components/ColorInput.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let value = '#000000';
|
||||
export let placeholder: string | null = null;
|
||||
export let required = false;
|
||||
export let icon: unknown = null;
|
||||
|
||||
let focus = false;
|
||||
</script>
|
||||
|
||||
<div class="label-input">
|
||||
{#if label?.length > 0}
|
||||
<label for={label}>{label}</label>
|
||||
{/if}
|
||||
|
||||
<div class="input" class:focus style={`--bg: ${value}`}>
|
||||
<div class="color-preview"></div>
|
||||
|
||||
<input
|
||||
id={label}
|
||||
{placeholder}
|
||||
name={label}
|
||||
style={`background-color: ${value}30`}
|
||||
bind:value
|
||||
{required}
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label-input {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: var(--bg);
|
||||
border-radius: inherit;
|
||||
border-bottom-left-radius: unset;
|
||||
border-bottom-right-radius: unset;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #a68b85;
|
||||
border-color: var(--bg);
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s;
|
||||
outline: none;
|
||||
|
||||
&.focus {
|
||||
box-shadow: 0px 0px 0px 4px #7d66654d;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touch-action: manipulation;
|
||||
background: transparent;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
--padding: 1rem;
|
||||
width: calc(100% - var(--padding));
|
||||
padding-left: var(--padding);
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
const healthy =
|
||||
status?.desiredNumberScheduled && status?.desiredNumberScheduled === status?.numberReady;
|
||||
const daemonUrl = `/cluster/daemonset/${metadata?.uid}`;
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="namespace">
|
||||
<h2>{pods?.length} of {metadata?.name} in {metadata?.namespace}</h2>
|
||||
<h2>{pods?.length} of <a href={daemonUrl}>{metadata?.name}</a> in {metadata?.namespace}</h2>
|
||||
</div>
|
||||
|
||||
<p>heatlthy: {healthy}</p>
|
||||
@@ -28,7 +29,7 @@
|
||||
.card-container {
|
||||
background-color: #cab2aa40;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
width: calc(100% - 1.5rem);
|
||||
padding: 0.75rem;
|
||||
|
||||
.namespace {
|
||||
@@ -38,8 +39,8 @@
|
||||
|
||||
.card-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr));
|
||||
gap: var(--grid-gap, 2rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
src/lib/components/DarkmodeToggle.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
function systemDarkModeEnabled() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle?.colorScheme != null) {
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateBodyClass() {
|
||||
document.body.className = darkmode ? "dark" : "light";
|
||||
}
|
||||
|
||||
let darkmode = $state(false);
|
||||
const darkmodeToggleIcon = $derived(darkmode ? "🌝" : "🌚");
|
||||
|
||||
function toggleDarkmode() {
|
||||
darkmode = !darkmode;
|
||||
updateBodyClass()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
darkmode = systemDarkModeEnabled()
|
||||
updateBodyClass()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="darkToggle">
|
||||
<span on:click={toggleDarkmode}>{ darkmodeToggleIcon }</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.darkToggle {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-right: 2px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,16 @@
|
||||
export let deploy: V1Deployment;
|
||||
|
||||
let { metadata, pods } = deploy;
|
||||
|
||||
const deploymentUrl = `/cluster/deployment/${metadata?.uid}`;
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="namespace">
|
||||
<h2>{metadata?.name} in {metadata?.namespace}</h2>
|
||||
<h2>
|
||||
<a href={deploymentUrl}>{metadata?.name}</a> in
|
||||
{metadata?.namespace}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-wrapper">
|
||||
@@ -34,8 +39,8 @@
|
||||
|
||||
.card-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
grid-template-columns: var(--grid-tmpl-cols, repeat(3, 1fr));
|
||||
gap: var(--grid-gap, 2rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
190
src/lib/components/Dialog.svelte
Normal file
@@ -0,0 +1,190 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { clickOutside } from '$lib/utils/mouseEvents';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string | null;
|
||||
close(): void
|
||||
}
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
close
|
||||
}: Props = $props()
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
const element = document.getElementsByClassName('dialog')?.[0]?.children[0];
|
||||
|
||||
if (clickOutside(event, element) === false) return;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
setTimeout(() => window.addEventListener('click', handleClick), 100);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
window.removeEventListener('click', handleClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-describedby="dialog-description"
|
||||
class="dialog"
|
||||
>
|
||||
<div tabindex="-1" id="dialog-title" class="title">
|
||||
{#if title.length || description?.length}
|
||||
<header>
|
||||
<button on:click={close} aria-disabled="false" aria-label="Close" type="button" tabindex="0"
|
||||
><svg viewBox="0 0 24 24" aria-hidden="true" tabindex="-1" height="100%" width="100%"
|
||||
><path
|
||||
d="M6.909 5.636a.9.9 0 1 0-1.273 1.273l5.091 5.09-5.091 5.092a.9.9 0 0 0 1.273 1.273L12 13.273l5.091 5.09a.9.9 0 1 0 1.273-1.272L13.273 12l5.09-5.091a.9.9 0 1 0-1.272-1.273L12 10.727z"
|
||||
></path></svg
|
||||
>
|
||||
</button>
|
||||
<h5>{title}</h5>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<main>
|
||||
{#if description}
|
||||
<div id="dialog-description">
|
||||
{@html description}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
<div class="alerts">
|
||||
<Success>There are no applications created yet.</Success>
|
||||
<Warning>There are no applications created yet.</Warning>
|
||||
<Error>There are no applications created yet.</Error>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.alerts > *) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
--offset-top: 4rem;
|
||||
padding-top: var(--offset-top);
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2006;
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
background-color: #7d666580;
|
||||
transition:
|
||||
opacity 0.4s ease,
|
||||
visibility 0.4s ease;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
padding: 1rem;
|
||||
width: calc(100vw - 2rem);
|
||||
height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
> div {
|
||||
max-width: 880px;
|
||||
// max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
--padding: 1rem;
|
||||
--background-color: #ffffff;
|
||||
--text-color: black;
|
||||
position: relative;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
background-clip: padding-box;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
transform: translate(0, 2rem);
|
||||
transition:
|
||||
transform 0.4s ease,
|
||||
opacity 0.4s ease;
|
||||
box-shadow:
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
pointer-events: auto;
|
||||
padding: var(--padding);
|
||||
width: calc(880px - calc(--padding * 2));
|
||||
z-index: 2008;
|
||||
max-width: 100%;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
|
||||
header {
|
||||
padding: 24px;
|
||||
padding: 24px 16px;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row-reverse;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
border: none;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
fill: orange;
|
||||
fill: #0a0a0a;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 auto 0 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
main #dialog-description {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
main > * {
|
||||
padding-top: 1rem;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
112
src/lib/components/Dropdown.svelte
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import { clickOutside } from '$lib/utils/mouseEvents';
|
||||
|
||||
export let options = ['Today', 'Yesterday', 'Last 7 Days', 'Last 30 Days', 'All time'];
|
||||
export let selected: string | undefined = undefined;
|
||||
export let placeholder = '';
|
||||
export let label = '';
|
||||
export let icon: unknown = undefined;
|
||||
export let required = false;
|
||||
|
||||
let dropdown: Element;
|
||||
let open = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function select(option: string) {
|
||||
selected = option;
|
||||
open = false;
|
||||
|
||||
dispatch('value', option);
|
||||
}
|
||||
|
||||
function handleEnter(event: KeyboardEvent): boolean {
|
||||
if (!(event.code === 'Enter' || event.code === 'Space')) return false;
|
||||
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
const outside = clickOutside(event, dropdown);
|
||||
if (outside === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
open = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('click', handleClick);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('click', handleClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dropdown" bind:this={dropdown}>
|
||||
<span role="button" class="trigger" on:click={() => (open = !open)}>
|
||||
<Input
|
||||
{icon}
|
||||
{placeholder}
|
||||
{label}
|
||||
value={selected}
|
||||
{required}
|
||||
on:blur={() => (open = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{#if open}
|
||||
<ul class="menu">
|
||||
{#each options as option (option)}
|
||||
<li>
|
||||
<span
|
||||
tabindex="0"
|
||||
class:active={selected === option}
|
||||
on:click={() => select(option)}
|
||||
on:keydown={(event) => handleEnter(event) && select(option)}
|
||||
role="button">{option}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
.arrow {
|
||||
margin-left: auto;
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: var(--bg);
|
||||
color: var(--color);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
list-style: none;
|
||||
margin: 0.2rem 0 0 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.menu li span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu li span:hover,
|
||||
.menu li span:active {
|
||||
background: #333;
|
||||
background: var(--color);
|
||||
color: var(--bg);
|
||||
}
|
||||
</style>
|
||||
23
src/lib/components/Error.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="error" class="negative">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.negative {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #1c1819;
|
||||
background: linear-gradient(90deg, #ff5449 4px, #fed8d0 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
345
src/lib/components/GlobalSearch.svelte
Normal file
@@ -0,0 +1,345 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Dialog from './Dialog.svelte';
|
||||
import Input from './Input.svelte';
|
||||
import { allRoutes } from '$lib/remote/filesystem.remote.ts';
|
||||
import type { PageRoute } from '$lib/remote/filesystem.remote.ts';
|
||||
|
||||
type ShortcutHandler = (event: KeyboardEvent) => void;
|
||||
interface Shortcut {
|
||||
keys: string[];
|
||||
handler: ShortcutHandler;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface MinimalElement {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface OverlayData {
|
||||
type: 'elements' | 'pages' | null;
|
||||
content: MinimalElement | unknown;
|
||||
}
|
||||
|
||||
class KeyboardShortcutManager {
|
||||
private shortcuts: Shortcut[] = [];
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
register(shortcut: Shortcut) {
|
||||
this.shortcuts.push(shortcut);
|
||||
}
|
||||
|
||||
unregisterAll() {
|
||||
this.shortcuts = [];
|
||||
window.removeEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
private handleKeydown = (event: KeyboardEvent) => {
|
||||
const pressedKeys = [
|
||||
event.metaKey ? 'Meta' : '',
|
||||
event.ctrlKey ? 'Control' : '',
|
||||
event.shiftKey ? 'Shift' : '',
|
||||
event.altKey ? 'Alt' : '',
|
||||
event.key.toUpperCase()
|
||||
].filter(Boolean);
|
||||
|
||||
for (const shortcut of this.shortcuts) {
|
||||
if (this.isMatch(shortcut.keys, pressedKeys)) {
|
||||
event.preventDefault();
|
||||
shortcut.handler(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// some other key, but not overlay is not open. Nothing to do
|
||||
if (!overlayStore.type) return;
|
||||
|
||||
// listen for text, any letter should reset focusIndex
|
||||
const singleLetter = (event.key.length == 1 && event.key.match(/\D/)) || 0 > 0;
|
||||
if (singleLetter) {
|
||||
updateFocus(0);
|
||||
}
|
||||
|
||||
// listen for number as shortcut actions
|
||||
const digit = event.key.match(/\d/)?.[0];
|
||||
if (digit?.length && digit?.length > 0) {
|
||||
setTimeout(() => {
|
||||
filterString = String(filterString)?.replaceAll(digit, '');
|
||||
}, 1);
|
||||
|
||||
updateFocus(Number(digit) - 1);
|
||||
}
|
||||
|
||||
// listen for arrow keys
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
||||
updateFocus(focusIndex + 1 * direction);
|
||||
}
|
||||
|
||||
// listen for enter key
|
||||
if (event.key === 'Enter' && filteredchildren.length > 0) {
|
||||
const { link, path } = filteredchildren[focusIndex];
|
||||
hideOverlay();
|
||||
openElement(link || path);
|
||||
}
|
||||
};
|
||||
|
||||
private isMatch(shortcutKeys: string[], pressedKeys: string[]) {
|
||||
return (
|
||||
shortcutKeys.length === pressedKeys.length &&
|
||||
shortcutKeys.every((key) => pressedKeys.includes(key))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let overlayStore: OverlayData = $state({ type: null, content: null });
|
||||
let pages: Array<PageRoute> = $state([]);
|
||||
|
||||
function showOverlay(type: 'elements' | 'pages', content: unknown) {
|
||||
if (type === overlayStore.type) return hideOverlay();
|
||||
|
||||
overlayStore = { type, content };
|
||||
focusSearchInput();
|
||||
filterString = '';
|
||||
updateFocus(0);
|
||||
}
|
||||
|
||||
function hideOverlay() {
|
||||
overlayStore = { type: null, content: null };
|
||||
}
|
||||
|
||||
async function resolvePages() {
|
||||
pages = await allRoutes();
|
||||
}
|
||||
|
||||
// call as soon as possible, even if blocking
|
||||
resolvePages();
|
||||
|
||||
// setup managers
|
||||
let manager: KeyboardShortcutManager;
|
||||
const className = 'search-container';
|
||||
|
||||
// search & filter
|
||||
let filterString = $state('');
|
||||
let focusIndex = $state(0);
|
||||
|
||||
let filteredchildren = $derived.by(() => {
|
||||
return overlayStore.content?.filter((a) => a?.name.toLowerCase().includes(filterString));
|
||||
});
|
||||
|
||||
const updateFocus = (index: number) => {
|
||||
if (index < 0) index = 0;
|
||||
else if (index >= filteredchildren.length) {
|
||||
index = filteredchildren.length - 1;
|
||||
}
|
||||
|
||||
focusIndex = index;
|
||||
};
|
||||
|
||||
// setup & register
|
||||
const focusSearchInput = () => {
|
||||
setTimeout(() => {
|
||||
const input = document.getElementsByClassName(className)[0]?.getElementsByTagName('input')[0];
|
||||
input.focus();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
function openElement(link: string) {
|
||||
if (String(link)?.startsWith('/')) {
|
||||
goto(link);
|
||||
} else {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
manager = new KeyboardShortcutManager();
|
||||
manager.register({
|
||||
keys: ['Meta', 'K'],
|
||||
handler: () => {
|
||||
if (!window || !('elements' in window)) return;
|
||||
const elements = window?.elements;
|
||||
showOverlay('elements', elements);
|
||||
}
|
||||
});
|
||||
|
||||
manager.register({
|
||||
keys: ['Meta', 'J'],
|
||||
handler: () => showOverlay('pages', pages)
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => manager?.unregisterAll());
|
||||
</script>
|
||||
|
||||
{#if overlayStore.type}
|
||||
<Dialog close={hideOverlay} title="" description="">
|
||||
<div class={className}>
|
||||
<Input label="" bind:value={filterString} placeholder="Search anything..." />
|
||||
|
||||
<ul>
|
||||
{#each filteredchildren as element, index (element)}
|
||||
<li
|
||||
class={index === focusIndex ? 'focus' : ''}
|
||||
on:click={() => 'link' in element && openElement(element.link ?? '/')}
|
||||
>
|
||||
<div class="header">
|
||||
<h3>{element?.name}</h3>
|
||||
<span class="sub">{element?.link}</span>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
{#each Object.entries(element) as [key, value] (key)}
|
||||
<span><b>{key}:</b> {value}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="hint">
|
||||
Press <span class="kbd">Esc</span> to close · <span class="kbd">⌘</span> +
|
||||
<span class="kbd">K</span> to toggle
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
:global(.dialog .title) {
|
||||
--padding: 1.2rem;
|
||||
--background-color: rgba(255, 255, 255, 0.92);
|
||||
--text-color: black;
|
||||
}
|
||||
|
||||
:global(body.dark .dialog .title) {
|
||||
--background-color: rgba(25, 25, 34, 0.92);
|
||||
--text-color: white;
|
||||
}
|
||||
|
||||
:global(.dialog .title .input) {
|
||||
--padding-w: 1rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 700px;
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
width: 100%;
|
||||
|
||||
ul {
|
||||
max-height: calc(50vh);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
margin-top: 0.3rem;
|
||||
padding-left: 0;
|
||||
overflow-y: scroll;
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100vh - var(--offset-top) * 4);
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
li {
|
||||
--blur: 18px;
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: background 0.15s;
|
||||
padding: 0.875rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
background-color: var(--card-bg);
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
gap: 0.375rem;
|
||||
|
||||
&.focus,
|
||||
&:hover {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 2px);
|
||||
border: 2px solid var(--theme);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.2px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.625rem 1.125rem;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
|
||||
b {
|
||||
color: var(--key);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Small footer hint */
|
||||
.hint {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
|
||||
.kbd {
|
||||
background-color: rgba(0, 0, 0, 0.16);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.search-container .label-input) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
overflow: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -8,14 +8,6 @@
|
||||
|
||||
return segments.map((segment, index) => {
|
||||
let label = decodeURI(segment);
|
||||
|
||||
// if not uuid pattern, this is weird order of ops
|
||||
/*
|
||||
if (!segment.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
|
||||
label = label.replace(/-/g, ' ')
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
label,
|
||||
path: '/' + segments.slice(0, index + 1).join('/')
|
||||
@@ -26,8 +18,10 @@
|
||||
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<!-- <img src="/logo.png" /> -->
|
||||
<h1>schleppe.cloud</h1>
|
||||
<a href="/">
|
||||
<!-- <img src="/logo.png" /> -->
|
||||
<h1>schleppe.cloud</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="middle crumbs">
|
||||
@@ -37,10 +31,6 @@
|
||||
<a href={crumb.path}>{crumb.label}</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<span>User profile</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -50,19 +40,19 @@
|
||||
left: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr auto;
|
||||
grid-template-columns: 200px 1fr auto;
|
||||
grid-template-areas: 'logoSection siteAndEnvironment profileAndHelp';
|
||||
align-items: center;
|
||||
background: #1c1819;
|
||||
background: var(--theme);
|
||||
padding: 0 1rem;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
color: var(--bg);
|
||||
margin: 1rem 0.5rem 0 0.5rem;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
z-index: 100;
|
||||
|
||||
&::after {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -76,6 +66,7 @@
|
||||
font-size: 1.5rem;
|
||||
padding: 0;
|
||||
font-weight: 300;
|
||||
color: var(--bg) !important;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -100,10 +91,11 @@
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
margin-left: 0.6rem;
|
||||
margin-left: 2rem;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seperator {
|
||||
@@ -111,5 +103,20 @@
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
top: -0.25rem;
|
||||
overflow: scroll;
|
||||
|
||||
.crumbs {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.right svg) {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
fill: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
96
src/lib/components/Input.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let placeholder: string;
|
||||
export let value: string = '';
|
||||
export let required = false;
|
||||
export let icon: unknown = null;
|
||||
|
||||
let focus = false;
|
||||
</script>
|
||||
|
||||
<div class="label-input">
|
||||
{#if label?.length > 0}
|
||||
<label for={label}>{label}</label>
|
||||
{/if}
|
||||
|
||||
<div class="input" class:focus>
|
||||
{#if icon}
|
||||
<i class="icon">
|
||||
<svelte:component this={icon} />
|
||||
</i>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
id={label}
|
||||
{placeholder}
|
||||
name={label}
|
||||
bind:value
|
||||
{required}
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(body.dark .label-input .input) {
|
||||
background: var(--highlight);
|
||||
}
|
||||
|
||||
.label-input {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.input i {
|
||||
display: block;
|
||||
--icon-size: 1.2rem;
|
||||
height: var(--icon-size);
|
||||
width: var(--icon-size);
|
||||
fill: #4c4243;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
--padding-h: 0.25rem;
|
||||
--padding-w: 0.75rem;
|
||||
width: calc(100% - (var(--padding-w) * 2));
|
||||
height: 2.5rem;
|
||||
background: #ffffff;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #a68b85;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--padding-h) var(--padding-w);
|
||||
|
||||
&.focus {
|
||||
box-shadow: 0px 0px 0px 4px #7d66654d;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
touch-action: manipulation;
|
||||
background: transparent;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
src/lib/components/JsonViewer.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { JsonView } from '@zerodevx/svelte-json-view';
|
||||
|
||||
export let json: any;
|
||||
</script>
|
||||
|
||||
<div class="parent">
|
||||
<div class="code">
|
||||
<JsonView {json} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.parent {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background-color: var(--color);
|
||||
max-height: calc(90vh - 10rem);
|
||||
color: white;
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
padding: 1rem;
|
||||
overflow: scroll;
|
||||
|
||||
--jsonBorderLeft: 1px dotted var(--highlight);
|
||||
--jsonKeyColor: #87afff;
|
||||
--jsonValStringColor: #afaf87;
|
||||
--jsonValBooleanColor: #ff8787;
|
||||
--jsonValNumberColor: #5fd787;
|
||||
}
|
||||
</style>
|
||||
96
src/lib/components/LXC.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import CubeSide from '$lib/icons/cube-side.svelte';
|
||||
import HardDrive from '$lib/icons/hard-disk.svelte';
|
||||
import Network from '$lib/icons/network.svelte';
|
||||
import CPU from '$lib/icons/cpu.svelte';
|
||||
import Fingerprint from '$lib/icons/fingerprint.svelte';
|
||||
import ExtractUp from '$lib/icons/extract-up.svelte';
|
||||
import InsertDown from '$lib/icons/insert-down.svelte';
|
||||
import Clock from '$lib/icons/clock.svelte';
|
||||
import Memory from '$lib/icons/floppy-disk.svelte';
|
||||
import { formatBytes, formatDuration } from '$lib/utils/conversion';
|
||||
import type { LXC } from '$lib/interfaces/proxmox';
|
||||
|
||||
const { lxc }: { lxc: LXC } = $props();
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<div class="icon"><CubeSide /></div>
|
||||
<span class="name">{lxc.name} <span class="subtle">{lxc.vmid}</span></span>
|
||||
|
||||
<span class={`status ${lxc.status === 'running' ? 'ok' : 'error'}`}></span>
|
||||
</div>
|
||||
|
||||
<div class="resource">
|
||||
<div class="title">
|
||||
<Network />
|
||||
<span>Status</span>
|
||||
</div>
|
||||
<span>{lxc.status}</span>
|
||||
|
||||
<div class="title">
|
||||
<Network />
|
||||
<span>CPUs</span>
|
||||
</div>
|
||||
<span>{lxc.cpus}</span>
|
||||
|
||||
<div class="title">
|
||||
<CPU />
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
<span>{Math.floor(lxc.cpu * 100) / 100}</span>
|
||||
|
||||
<div class="title">
|
||||
<HardDrive />
|
||||
<span>Max Disk</span>
|
||||
</div>
|
||||
<span>{formatBytes(lxc.maxdisk)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Memory />
|
||||
<span>Memory</span>
|
||||
</div>
|
||||
<span>{formatBytes(lxc.mem)}</span>
|
||||
|
||||
<div class="title">
|
||||
<InsertDown />
|
||||
<span>Net In</span>
|
||||
</div>
|
||||
<span>{formatBytes(lxc.netin)}</span>
|
||||
|
||||
<div class="title">
|
||||
<ExtractUp />
|
||||
<span>Net Out</span>
|
||||
</div>
|
||||
<span>{formatBytes(lxc.netout / 8)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Clock />
|
||||
<span>Uptime</span>
|
||||
</div>
|
||||
<span>{formatDuration(lxc.uptime)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Fingerprint />
|
||||
<span>lxc ID</span>
|
||||
</div>
|
||||
<span>{lxc.vmid}</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<button on:click={() => goto(`/servers/lxc/${lxc.vmid}`)}>
|
||||
<span>Pod details</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/card.scss';
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
max-width: 550px;
|
||||
}
|
||||
</style>
|
||||
115
src/lib/components/LiveImage.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { grey400x225 } from '$lib/utils/staticImageSource';
|
||||
import Dialog from './Dialog.svelte';
|
||||
|
||||
const IMAGE_REFRESH_INTERVAL = 1000;
|
||||
|
||||
let { imageUrl }: { imageUrl: string } = $props();
|
||||
let lastUpdated = new Date();
|
||||
let timestamp = $state(0);
|
||||
let fullscreen = $state(false);
|
||||
|
||||
let imageSource: string | ArrayBuffer = grey400x225;
|
||||
let lastCacheSize: string;
|
||||
|
||||
function loadBlob(blob: Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const img = document.getElementById('live-image') as HTMLImageElement;
|
||||
if (!img) return;
|
||||
|
||||
imageSource = reader?.result || '';
|
||||
if (imageSource === '') {
|
||||
console.log('no image data, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
// set imageSource to image element
|
||||
img.src = `data:image/jpeg;base64; ${imageSource}`;
|
||||
lastUpdated = new Date();
|
||||
};
|
||||
|
||||
// load blob into FileReader
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
function refetchImage() {
|
||||
let url;
|
||||
try {
|
||||
const { protocol, host } = window.location;
|
||||
url = new URL(`${protocol}//${host}/image-proxy/${imageUrl}`);
|
||||
} catch {
|
||||
console.log('url not valid, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'If-None-Match': lastCacheSize
|
||||
}
|
||||
};
|
||||
|
||||
fetch(url.href, options)
|
||||
.then((resp) => {
|
||||
if (resp.status === 304) throw Error('image exists');
|
||||
lastCacheSize = resp.headers.get('Content-Length') || '';
|
||||
return resp;
|
||||
})
|
||||
.then((resp) => resp.blob())
|
||||
.then((blob) => loadBlob(blob))
|
||||
.catch(() => {}); // suppress all exceptions
|
||||
}
|
||||
|
||||
function timeDiff(d: Date) {
|
||||
const seconds = d.getTime();
|
||||
const v = seconds - lastUpdated.getTime();
|
||||
return Math.floor(v / 100) / 10;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refetchImage();
|
||||
const imageInterval = setInterval(refetchImage, IMAGE_REFRESH_INTERVAL);
|
||||
const timerInterval = setInterval(() => (timestamp = timeDiff(new Date())), 80);
|
||||
|
||||
return () => Promise.all([clearInterval(imageInterval), clearInterval(timerInterval)]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="liveimage">
|
||||
{#if !fullscreen}
|
||||
<img on:click={() => (fullscreen = !fullscreen)} src={String(imageSource)} id="live-image" />
|
||||
{:else}
|
||||
<div class="fullscreen-container">
|
||||
<Dialog title="Live stream of printer" close={() => (fullscreen = false)}>
|
||||
<img src={String(imageSource)} id="live-image" />
|
||||
<span>Last update {timestamp}s ago</span>
|
||||
</Dialog>
|
||||
|
||||
<img src={String(grey400x225)} />
|
||||
</div>
|
||||
{/if}
|
||||
<span>Last update {timestamp}s ago</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.liveimage img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(.fullscreen-container .dialog img) {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
:global(.fullscreen-container #dialog-title) {
|
||||
max-width: 98vw;
|
||||
}
|
||||
</style>
|
||||
@@ -69,133 +69,5 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
max-width: 550px;
|
||||
|
||||
background: #fbf6f4;
|
||||
box-shadow: var(
|
||||
--str-shadow-s,
|
||||
0px 0px 2px #22242714,
|
||||
0px 1px 4px #2224271f,
|
||||
0px 4px 8px #22242729
|
||||
);
|
||||
pointer-events: all;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
|
||||
.icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
|
||||
&.ok {
|
||||
background-color: var(--positive);
|
||||
}
|
||||
&.warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
&.error {
|
||||
background-color: var(--negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.5rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.resource {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg);
|
||||
|
||||
row-gap: 6px;
|
||||
column-gap: 20px;
|
||||
|
||||
> div,
|
||||
span {
|
||||
display: flex;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.resource .title svg) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
margin-top: auto;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: unset;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 0 0.5rem;
|
||||
flex: 1;
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
border: 1px solid #eaddd5;
|
||||
border-radius: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.1s ease;
|
||||
will-change: box-shadow 0.25s;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #077c35;
|
||||
}
|
||||
@import "../styles/card.scss";
|
||||
</style>
|
||||
|
||||
113
src/lib/components/PageElement.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
export let bgColor: string;
|
||||
export let color = 'black';
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let header = '';
|
||||
export let link = '/';
|
||||
export let icon = '';
|
||||
</script>
|
||||
|
||||
<a href={link} class="shortcut" style={`--bg: ${bgColor}; --color: ${color}`}>
|
||||
<span class="header">{header}</span>
|
||||
<i class="icon">{icon}</i>
|
||||
<h2>{title}</h2>
|
||||
|
||||
<span class="description">{description}</span>
|
||||
<button>Utforsk</button>
|
||||
</a>
|
||||
|
||||
<style lang="scss">
|
||||
.shortcut {
|
||||
aspect-ratio: 1.5 / 1;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color);
|
||||
background-color: var(--bg);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
padding: 2rem 1.25rem;
|
||||
|
||||
@media (min-width: 750px) {
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: 2.5rem 1.875rem 3.125rem;
|
||||
|
||||
.header {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
padding: 3.125rem 3.75rem 3.75rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) translateX(-4px);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 4.5rem;
|
||||
letter-spacing: 4.4px;
|
||||
font-family: 'Stop';
|
||||
margin: 0;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
flex: unset;
|
||||
color: var(--color);
|
||||
font-size: 1rem;
|
||||
border: 2px solid var(--color);
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 0 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
color: var(--bg);
|
||||
background-color: var(--color);
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
// padding: 0 3rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
button::after {
|
||||
position: absolute;
|
||||
transition: inherit;
|
||||
width: fit-content;
|
||||
left: 2rem;
|
||||
top: 12px;
|
||||
opacity: 0;
|
||||
content: '→';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import Network from '$lib/icons/network.svelte';
|
||||
import Layers from '$lib/icons/layers.svelte';
|
||||
import Clock from '$lib/icons/clock.svelte';
|
||||
import Sync from '$lib/icons/sync.svelte';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
@@ -33,12 +34,17 @@
|
||||
// set uptime
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
function idlePhase(phase: string | undefined) {
|
||||
const phases = ['Failed', 'Succeeded'];
|
||||
return phases.includes(phase || '');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<div class={`card ${idlePhase(status?.phase) && 'not-running'}`}>
|
||||
<div class="header">
|
||||
<div class="icon"><Layers /></div>
|
||||
<span class="name">{name}</span>
|
||||
@@ -67,6 +73,12 @@
|
||||
</div>
|
||||
<span>{i + 1} of {replicas}</span>
|
||||
|
||||
<div class="title">
|
||||
<Sync />
|
||||
<span>Restarts</span>
|
||||
</div>
|
||||
<span>{status?.containerStatuses?.[0].restartCount}</span>
|
||||
|
||||
<div class="title">
|
||||
<Connection />
|
||||
<span>Running on Node</span>
|
||||
@@ -93,6 +105,8 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../styles/card.scss";
|
||||
|
||||
.card-container {
|
||||
background-color: #cab2aa40;
|
||||
border-radius: 0.5rem;
|
||||
@@ -110,94 +124,4 @@
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
max-width: 550px;
|
||||
|
||||
background: #fbf6f4;
|
||||
box-shadow: var(
|
||||
--str-shadow-s,
|
||||
0px 0px 2px #22242714,
|
||||
0px 1px 4px #2224271f,
|
||||
0px 4px 8px #22242729
|
||||
);
|
||||
pointer-events: all;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
|
||||
.icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
|
||||
&.ok {
|
||||
background-color: var(--positive);
|
||||
}
|
||||
&.warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
&.error {
|
||||
background-color: var(--negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.5rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.resource {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg);
|
||||
|
||||
row-gap: 6px;
|
||||
column-gap: 20px;
|
||||
|
||||
> div,
|
||||
span {
|
||||
display: flex;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.resource .title svg) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
margin-top: auto;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #077c35;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,10 +4,17 @@
|
||||
</script>
|
||||
|
||||
<article class="main-container">
|
||||
<div class="header">
|
||||
<h2>{title}</h2>
|
||||
<label>{description}</label>
|
||||
</div>
|
||||
{#if title || description}
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<h2>{title}</h2>
|
||||
<slot name="top-left" />
|
||||
</div>
|
||||
{#if description && description?.length > 0}
|
||||
<label>{description}</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot></slot>
|
||||
</article>
|
||||
@@ -25,5 +32,10 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,21 @@
|
||||
import { formatBytes, formatDuration } from '$lib/utils/conversion';
|
||||
import type { Node } from '$lib/interfaces/proxmox';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Speed from '$lib/icons/speed.svelte';
|
||||
import Fingerprint from '$lib/icons/fingerprint.svelte';
|
||||
|
||||
export let node: Node;
|
||||
|
||||
const buttons = ['View logs', 'Web terminal', 'graphs'];
|
||||
const buttons = [
|
||||
{ name: 'View logs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:25::::::` },
|
||||
{
|
||||
name: 'Terminal',
|
||||
link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:=jsconsole::::::`
|
||||
},
|
||||
{ name: 'Graphs', link: `https://${node.ip}:8006/#v1:0:=node%2F${node.name}:4:5::::::` },
|
||||
{ name: 'Details', link: `/servers/node/${node.name}` }
|
||||
];
|
||||
|
||||
let { cpuinfo, memory, uptime, loadavg } = node.info;
|
||||
|
||||
@@ -23,6 +34,9 @@
|
||||
const lxcsRunning = node.lxcs.filter((l) => l?.template !== 1 && l.status === 'running');
|
||||
const lxcsTotal = node.lxcs.filter((l) => l?.template !== 1);
|
||||
|
||||
const t = cpuinfo.model.match(/(\w+\(\w+\)) (\w+\(\w+\)) (.*)/);
|
||||
const cpu = t[3].replaceAll(' ', ' ');
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => (uptime += 1), 1000);
|
||||
});
|
||||
@@ -51,6 +65,18 @@
|
||||
>{cpuinfo.cpus} Cores on {cpuinfo.sockets} {cpuinfo.sockets > 1 ? 'Sockets' : 'Socket'}</span
|
||||
>
|
||||
|
||||
<div class="title">
|
||||
<Fingerprint />
|
||||
<span>Model</span>
|
||||
</div>
|
||||
<span>{cpu}</span>
|
||||
|
||||
<div class="title">
|
||||
<Speed />
|
||||
<span>Turbo speed</span>
|
||||
</div>
|
||||
<span>{Math.floor(node.info.cpuinfo.mhz) / 1000} GHz</span>
|
||||
|
||||
<div class="title">
|
||||
<Shield />
|
||||
<span>DDoS protection</span>
|
||||
@@ -90,170 +116,15 @@
|
||||
|
||||
<div class="footer">
|
||||
{#each buttons as btn (btn)}
|
||||
<button on:click={() => console.log(node)}>
|
||||
<span>{btn}</span>
|
||||
</button>
|
||||
<a href={btn.link} target={btn.link[0] === '/' ? '' : '_blank'} rel="noopener noreferrer">
|
||||
<button>
|
||||
<span>{btn.name}</span>
|
||||
</button>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes pulse-live {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0.7);
|
||||
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(0, 212, 57, 0);
|
||||
box-shadow: 0 0 0 10px rgba(0, 212, 57, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0);
|
||||
box-shadow: 0 0 0 0 rgba(0, 212, 57, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin pulse-dot {
|
||||
&::after {
|
||||
content: '';
|
||||
top: 50%;
|
||||
margin-left: 0.4rem;
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: pulse-live 2s infinite;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
.card {
|
||||
background: #fbf6f4;
|
||||
box-shadow: var(
|
||||
--str-shadow-s,
|
||||
0px 0px 2px #22242714,
|
||||
0px 1px 4px #2224271f,
|
||||
0px 4px 8px #22242729
|
||||
);
|
||||
pointer-events: all;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
|
||||
.icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
|
||||
&.ok {
|
||||
--color: var(--positive);
|
||||
@include pulse-dot;
|
||||
}
|
||||
&.warning {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
&.error {
|
||||
background-color: var(--negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.5rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.resource {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--bg);
|
||||
|
||||
row-gap: 6px;
|
||||
column-gap: 20px;
|
||||
|
||||
> div,
|
||||
span {
|
||||
display: flex;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.resource .title svg) {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
margin-top: auto;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: unset;
|
||||
border-radius: 0.5rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 0 0.5rem;
|
||||
flex: 1;
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
border: 1px solid #eaddd5;
|
||||
border-radius: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.1s ease;
|
||||
will-change: box-shadow 0.25s;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #077c35;
|
||||
}
|
||||
@import '../styles/card.scss';
|
||||
</style>
|
||||
|
||||
50
src/lib/components/ServerSummary.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { formatBytes } from '$lib/utils/conversion';
|
||||
import type { Node } from '$lib/interfaces/proxmox';
|
||||
|
||||
export let nodes: Node[];
|
||||
|
||||
const totalCpu = nodes.map((n) => n.info.cpuinfo.cpus).reduce((a, b) => a + b, 0);
|
||||
const totalMem = nodes.map((n) => n.info.memory.total).reduce((a, b) => a + b, 0);
|
||||
const vmState = {
|
||||
total: nodes.map((n) => n.vms.filter((v) => v?.template !== 1)).flat(2).length,
|
||||
running: nodes
|
||||
.map((n) => n.vms.filter((v) => v?.template !== 1 && v.status === 'running'))
|
||||
.flat(2).length
|
||||
};
|
||||
|
||||
const lxcState = {
|
||||
total: nodes.map((n) => n.lxcs.filter((l) => l?.template !== 1)).flat(2).length,
|
||||
running: nodes
|
||||
.map((n) => n.lxcs.filter((l) => l?.template !== 1 && l.status === 'running'))
|
||||
.flat(2).length
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="main-container">
|
||||
<span>CPUs: <b>{totalCpu}</b></span>
|
||||
<span>Memory: <b>{formatBytes(totalMem)}</b></span>
|
||||
<span>VMs: <b>{vmState.running}/{vmState.total}</b></span>
|
||||
<span>LXCs: <b>{lxcState.running}/{lxcState.total}</b></span>
|
||||
<span>Nodes: <b>{nodes.length}</b></span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.main-container {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
b {
|
||||
font-family: 'Reckless Neue';
|
||||
font-size: 1.3rem;
|
||||
color: var(--theme);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import { allRoutes } from '$lib/remote/filesystem.remote.ts';
|
||||
|
||||
const pages = [
|
||||
{
|
||||
name: 'Home',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
name: 'Sites',
|
||||
path: '/sites'
|
||||
},
|
||||
{
|
||||
name: 'Servers',
|
||||
path: '/servers'
|
||||
},
|
||||
{
|
||||
name: 'Printer',
|
||||
path: '/printer'
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
path: '/network'
|
||||
},
|
||||
{
|
||||
name: 'Cluster',
|
||||
path: '/cluster'
|
||||
},
|
||||
{
|
||||
name: 'Health',
|
||||
path: '/health'
|
||||
}
|
||||
];
|
||||
let mobileNavOpen = $state(false);
|
||||
let pages = $state([]);
|
||||
|
||||
async function resolvePages() {
|
||||
pages = await allRoutes();
|
||||
}
|
||||
|
||||
resolvePages();
|
||||
|
||||
const activePage = derived(page, ($page) => $page.url.pathname);
|
||||
const toggle = () => {
|
||||
mobileNavOpen = !mobileNavOpen;
|
||||
|
||||
// nav opens
|
||||
if (mobileNavOpen) {
|
||||
const index = pages.findIndex((page) => $activePage === page.path);
|
||||
const el: HTMLElement = document.getElementsByTagName('nav')[0].getElementsByTagName('a')[
|
||||
index
|
||||
];
|
||||
if (!el) return;
|
||||
|
||||
setTimeout(() => {
|
||||
el?.scrollIntoViewIfNeeded();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="nav-wrapper">
|
||||
<div class={`nav-wrapper ${mobileNavOpen ? 'open' : ''}`}>
|
||||
<nav>
|
||||
{#each pages as page, i (page.name)}
|
||||
{#if i === 0}
|
||||
<a class={$activePage === page.path ? 'highlight' : ''} href={page.path}>{page.name}</a>
|
||||
<a
|
||||
class={$activePage === page.path ? 'highlight' : ''}
|
||||
on:click={() => (mobileNavOpen = false)}
|
||||
href={page.path}>{page.name}</a
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
class={`${$activePage !== page.path && $activePage.startsWith(page.path) ? 'child' : ''} ${$activePage.startsWith(page.path) ? 'highlight' : ''}`}
|
||||
on:click={() => (mobileNavOpen = false)}
|
||||
href={page.path}>{page.name}</a
|
||||
>
|
||||
{/if}
|
||||
@@ -51,25 +51,145 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="mobile-nav-toggle" class={mobileNavOpen ? 'open' : ''} on:click={toggle}>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
#mobile-nav-toggle {
|
||||
--size: 3rem;
|
||||
--padding: 2rem;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
right: 1rem;
|
||||
bottom: var(--padding);
|
||||
background-color: var(--color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
@media screen and (min-width: 750px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
border-radius: 4px;
|
||||
height: 3px;
|
||||
background-color: var(--highlight);
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
&.open,
|
||||
&:hover {
|
||||
height: calc(var(--size) + 4px);
|
||||
width: calc(var(--size) + 4px);
|
||||
margin-bottom: -2px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
&.open {
|
||||
span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
transform: rotate(45deg);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
opacity: 0%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
span:nth-child(3) {
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
--nav-width: 240px;
|
||||
top: 72px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: var(--nav-width);
|
||||
margin-right: 1rem;
|
||||
transition: all 0.4s ease-in-out;
|
||||
z-index: 99;
|
||||
border-radius: var(--border-radius);
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
max-height: 66vh;
|
||||
height: fit-content;
|
||||
background-color: var(--bg);
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
--nav-width: 100px;
|
||||
@media screen and (max-width: 1460px) {
|
||||
--nav-width: 220px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
--nav-width: 140px;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* mobile */
|
||||
@media screen and (max-width: 750px) {
|
||||
position: fixed;
|
||||
right: -100%;
|
||||
padding-top: 0;
|
||||
padding: 0.51rem;
|
||||
top: calc(72px + 1rem);
|
||||
|
||||
&.open {
|
||||
right: 0.5rem;
|
||||
right: -1rem;
|
||||
|
||||
background-color: var(--color);
|
||||
|
||||
overflow-y: scroll;
|
||||
|
||||
nav a {
|
||||
color: white;
|
||||
|
||||
&:hover,
|
||||
&.highlight {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.dark .nav-wrapper.open) {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: var(--bg);
|
||||
|
||||
nav a {
|
||||
&:hover,
|
||||
&.highlight {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
/* position: fixed; */
|
||||
width: var(--nav-width);
|
||||
gap: 4px;
|
||||
margin-top: 1rem;
|
||||
|
||||
23
src/lib/components/Success.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="warning" class="warning">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.warning {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #091d14;
|
||||
background: linear-gradient(90deg, #02a73a 4px, #62fe67 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let columns: Array<string> | object;
|
||||
@@ -8,7 +6,6 @@
|
||||
export let links: Array<string> = [];
|
||||
export let footer = '';
|
||||
|
||||
const hasLinks = links?.length > 0;
|
||||
let displayColumns: string[] = [];
|
||||
if (typeof columns === 'object' && !Array.isArray(columns)) {
|
||||
displayColumns = Object.values(columns);
|
||||
@@ -17,13 +14,17 @@
|
||||
</script>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="header">
|
||||
<h2>{title}</h2>
|
||||
<div class="description">{description}</div>
|
||||
</div>
|
||||
{#if title?.length || description?.length}
|
||||
<div class="header">
|
||||
<h2>{title}</h2>
|
||||
<div class="description">{description}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
<slot></slot>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -38,23 +39,8 @@
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data as row, i (row)}
|
||||
<tr on:click={() => hasLinks && goto(links[i])} class={hasLinks ? 'link' : ''}>
|
||||
{#each columns as column (column)}
|
||||
{#if column === 'Link'}
|
||||
<td><a href={row[column]}>Link</a></td>
|
||||
{:else if column === 'Hex'}
|
||||
<td><span class="color" style={`background: ${row[column]}`} /></td>
|
||||
{:else if Array.isArray(row[column])}
|
||||
<td>{row[column].join(', ')}</td>
|
||||
{:else}
|
||||
<td>{row[column]}</td>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
<slot name="tbody"></slot>
|
||||
</table>
|
||||
|
||||
{#if footer?.length}
|
||||
@@ -69,7 +55,7 @@
|
||||
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
opacity: 0.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -77,59 +63,6 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: sans-serif;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-stretch: 2px;
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
tr {
|
||||
&:not(&:last-of-type) {
|
||||
border-bottom: 1px solid #eaddd5;
|
||||
}
|
||||
|
||||
&:hover > td {
|
||||
background-color: var(--highlight);
|
||||
background-color: #f5ede9;
|
||||
}
|
||||
|
||||
&.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.color {
|
||||
--size: 2rem;
|
||||
display: block;
|
||||
width: calc(var(--size) * 2);
|
||||
height: var(--size);
|
||||
margin-top: -calc(var(--size / 2));
|
||||
margin-bottom: -calc(var(--size / 2));
|
||||
border-radius: var(--border-radius, 1rem);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
0
src/lib/components/TableWrapper.svelte
Normal file
133
src/lib/components/ThumbnailButton.svelte
Normal file
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import External from '$lib/icons/external.svelte';
|
||||
import type { Site } from '$lib/interfaces/site.ts';
|
||||
|
||||
|
||||
let { title, image, background, color, link }: Site = $props();
|
||||
|
||||
let colors = [
|
||||
['#401C26', '#f6cfdd'],
|
||||
['#213726', '#BDCBB2'],
|
||||
['#EED7CD', '#262221'],
|
||||
['#262221', '#F3BFA2'],
|
||||
['#f6cfdd', '#401C26'],
|
||||
['#BDCBB2', '#213726'],
|
||||
['#FF8FAB', '#401C26'],
|
||||
['#9381FF', '#262221']
|
||||
];
|
||||
|
||||
if (!background && !color) {
|
||||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
background = randomColor[0];
|
||||
color = randomColor[1];
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={link}
|
||||
style={`--background: ${background}; --color: ${color}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="image" style={`background-image: url("${image}")`}></div>
|
||||
<hr />
|
||||
|
||||
<div class="title">
|
||||
<h2>
|
||||
{title || 'Grafana'}
|
||||
<span class="link"><External /></span>
|
||||
</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
border-radius: 0.8rem;
|
||||
color: white;
|
||||
text-align: justify;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
|
||||
&,
|
||||
h2,
|
||||
.link,
|
||||
.title {
|
||||
transition: all 0.18s ease-in-out;
|
||||
}
|
||||
|
||||
.title {
|
||||
h2 {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: 1.2rem auto;
|
||||
font-size: 2rem;
|
||||
justify-content: center;
|
||||
text-transform: lowercase;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: unset;
|
||||
border: none;
|
||||
width: 96%;
|
||||
margin-left: 2%;
|
||||
height: 1px;
|
||||
background-color: var(--color);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 8rem;
|
||||
width: 100%;
|
||||
width: 8rem;
|
||||
margin: 1.2rem auto;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
position: absolute;
|
||||
transition: inherit;
|
||||
width: fit-content;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
left: calc(100% - 2rem);
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
fill: var(--color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.title h2 {
|
||||
padding: 0.6rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) translateX(-4px);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.8rem;
|
||||
|
||||
.title h2 {
|
||||
padding-right: 2rem;
|
||||
|
||||
.link {
|
||||
opacity: 1;
|
||||
left: calc(100% - 1rem);
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/lib/components/VM.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import Desktop from '$lib/icons/desktop.svelte';
|
||||
import HardDrive from '$lib/icons/hard-disk.svelte';
|
||||
import Network from '$lib/icons/network.svelte';
|
||||
import CPU from '$lib/icons/cpu.svelte';
|
||||
import Fingerprint from '$lib/icons/fingerprint.svelte';
|
||||
import ExtractUp from '$lib/icons/extract-up.svelte';
|
||||
import InsertDown from '$lib/icons/insert-down.svelte';
|
||||
import Clock from '$lib/icons/clock.svelte';
|
||||
import Memory from '$lib/icons/floppy-disk.svelte';
|
||||
import { formatBytes, formatDuration } from '$lib/utils/conversion';
|
||||
import type { VM } from '$lib/interfaces/proxmox';
|
||||
const { vm }: { vm: VM } = $props();
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<div class="icon"><Desktop /></div>
|
||||
<span class="name">{vm.name} <span class="subtle">{vm.vmid}</span></span>
|
||||
|
||||
<span class={`status ${vm.status === 'running' ? 'ok' : 'error'}`}></span>
|
||||
</div>
|
||||
|
||||
<div class="resource">
|
||||
<div class="title">
|
||||
<Network />
|
||||
<span>Status</span>
|
||||
</div>
|
||||
<span>{vm.status}</span>
|
||||
|
||||
<div class="title">
|
||||
<Network />
|
||||
<span>CPUs</span>
|
||||
</div>
|
||||
<span>{vm.cpus}</span>
|
||||
|
||||
<div class="title">
|
||||
<CPU />
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
<span>{Math.floor(vm.cpu * 100) / 100}</span>
|
||||
|
||||
<div class="title">
|
||||
<HardDrive />
|
||||
<span>Max Disk</span>
|
||||
</div>
|
||||
<span>{formatBytes(vm.maxdisk)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Memory />
|
||||
<span>Memory</span>
|
||||
</div>
|
||||
<span>{formatBytes(vm.mem)}</span>
|
||||
|
||||
<div class="title">
|
||||
<InsertDown />
|
||||
<span>Net In</span>
|
||||
</div>
|
||||
<span>{formatBytes(vm.netin)}</span>
|
||||
|
||||
<div class="title">
|
||||
<ExtractUp />
|
||||
<span>Net Out</span>
|
||||
</div>
|
||||
<span>{formatBytes(vm.netout / 8)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Clock />
|
||||
<span>Uptime</span>
|
||||
</div>
|
||||
<span>{formatDuration(vm.uptime)}</span>
|
||||
|
||||
<div class="title">
|
||||
<Fingerprint />
|
||||
<span>VM ID</span>
|
||||
</div>
|
||||
<span>{vm.vmid}</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<button on:click={() => goto(`/servers/vm/${vm.name}`)}>
|
||||
<span>VM details</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/card.scss';
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
max-width: 550px;
|
||||
}
|
||||
</style>
|
||||
23
src/lib/components/Warning.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<div type="warning" class="warning">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.warning {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
color: #1c1819;
|
||||
background: linear-gradient(90deg, #db7700 4px, #fedd6c 4px);
|
||||
|
||||
> div {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/lib/components/forms/FormDNS.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import TextSize from '$lib/icons/text-size.svelte';
|
||||
import Quill from '$lib/icons/quill.svelte';
|
||||
import Tag from '$lib/icons/tag.svelte';
|
||||
import Dropdown from '../Dropdown.svelte';
|
||||
|
||||
const { close }: { close: () => void } = $props();
|
||||
|
||||
const dnsTypes = ['A', 'AAAA', 'CNAME', 'NS', 'SOA', 'MX', 'TXT', 'PTR'];
|
||||
</script>
|
||||
|
||||
<form method="POST">
|
||||
<div class="wrapper">
|
||||
<Input label="Name" icon={TextSize} placeholder="Website name" required />
|
||||
<Dropdown
|
||||
placeholder="Record type"
|
||||
label="Type"
|
||||
required={true}
|
||||
icon={Tag}
|
||||
options={dnsTypes}
|
||||
/>
|
||||
<Input label="Content" icon={Quill} placeholder="IPv4 address" required />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
|
||||
><span tabindex="-1">Cancel</span></button
|
||||
>
|
||||
<button class="affirmative" type="submit" tabindex="-1">
|
||||
<span tabindex="-1">Add record</span>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 0.75rem 1.5rem;
|
||||
max-width: 100%;
|
||||
height: 2.5rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(form .wrapper div) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
82
src/lib/components/forms/FormFilament.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import ColorInput from '$lib/components/ColorInput.svelte';
|
||||
import Dropdown from '$lib/components/Dropdown.svelte';
|
||||
import Flower from '$lib/icons/flower.svelte';
|
||||
import Weight from '$lib/icons/weight.svelte';
|
||||
import Link from '$lib/icons/link.svelte';
|
||||
import PencilRuler from '$lib/icons/pencil-ruler.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const close = () => dispatch('close');
|
||||
|
||||
const materialOptions = ['PLA Matte', 'PLA Basic', 'PLA-CF', 'PET-G'];
|
||||
const weightOptions = ['0.5 kg', '1 kg', '2 kg'];
|
||||
let process = $state('');
|
||||
let port = $state('');
|
||||
</script>
|
||||
|
||||
<form method="POST" action="/printer/filament">
|
||||
<div class="wrapper">
|
||||
<ColorInput label="Hex" required />
|
||||
<Input label="Color name" icon={Flower} placeholder="Infinity orange" required />
|
||||
<Dropdown
|
||||
placeholder="Plastic material name"
|
||||
label="Material"
|
||||
required={true}
|
||||
icon={PencilRuler}
|
||||
options={materialOptions}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Spool weight"
|
||||
label="Weight"
|
||||
icon={Weight}
|
||||
required={true}
|
||||
options={weightOptions}
|
||||
/>
|
||||
<Input label="Link" icon={Link} placeholder="https://store.shop/item" required />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
|
||||
><span tabindex="-1">Cancel</span></button
|
||||
>
|
||||
<button class="affirmative" type="submit" tabindex="-1">
|
||||
<span tabindex="-1">Add connection</span>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 0.75rem 1.5rem;
|
||||
max-width: 100%;
|
||||
height: 2.5rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(form .wrapper div) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
71
src/lib/components/forms/FormSite.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Input from '$lib/components/Input.svelte';
|
||||
import ColorInput from '$lib/components/ColorInput.svelte';
|
||||
import Id from '$lib/icons/id.svelte';
|
||||
import TextColor from '$lib/icons/text-color.svelte';
|
||||
import TextSize from '$lib/icons/text-size.svelte';
|
||||
import Window from '$lib/icons/window.svelte';
|
||||
import Quill from '$lib/icons/quill.svelte';
|
||||
import Tag from '$lib/icons/tag.svelte';
|
||||
import Link from '$lib/icons/link.svelte';
|
||||
import PaintRoller from '$lib/icons/paint-roller.svelte';
|
||||
import Bucket from '$lib/icons/bucket.svelte';
|
||||
import Picture from '$lib/icons/picture.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const close = () => dispatch('close');
|
||||
</script>
|
||||
|
||||
<form method="POST">
|
||||
<div class="wrapper">
|
||||
<Input label="Name" icon={TextSize} placeholder="Website name" required />
|
||||
<Input label="Link" icon={Link} placeholder="https://site.tld" required />
|
||||
<Input label="Image" icon={Picture} placeholder="/images/site.png" required />
|
||||
<ColorInput label="Color" icon={PaintRoller} placeholder="#21ADF6" required />
|
||||
<ColorInput label="Background" icon={Bucket} />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
|
||||
><span tabindex="-1">Cancel</span></button
|
||||
>
|
||||
<button class="affirmative" type="submit" tabindex="-1">
|
||||
<span tabindex="-1">Add connection</span>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 0.75rem 1.5rem;
|
||||
max-width: 100%;
|
||||
height: 2.5rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
flex: unset;
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(form .wrapper div) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
98
src/lib/components/kube-describe/DaemonSet.svelte
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { V1DaemonSet } from '@kubernetes/client-node';
|
||||
|
||||
const { daemonset }: { daemonset: V1DaemonSet } = $props();
|
||||
const { status, spec } = daemonset || {};
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabView>
|
||||
<div class="section-wrapper">
|
||||
<Section title="Status" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Pods scheduled</label>
|
||||
<span>{status?.currentNumberScheduled}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods available</label>
|
||||
<span>{status?.numberAvailable}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods ready</label>
|
||||
<span>{status?.numberReady}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods misscheduled</label>
|
||||
<span>{status?.numberMisscheduled}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Spec" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Number of containers</label>
|
||||
<span>{spec?.template?.spec?.containers.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Number of volumes</label>
|
||||
<span>{spec?.template?.spec?.volumes?.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Restart policy</label>
|
||||
<span>{spec?.template?.spec?.restartPolicy}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Host network</label>
|
||||
<span>{spec?.template?.spec?.hostNetwork ? 'yes' : 'no'}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>DNS policy</label>
|
||||
<span>{spec?.dnsPolicy}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.metadata} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.spec} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={daemonset.status} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
109
src/lib/components/kube-describe/Deployment.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import type { V1Deployment } from '@kubernetes/client-node';
|
||||
|
||||
const { deployment }: { deployment: V1Deployment } = $props();
|
||||
const { status, metadata, spec } = deployment || {};
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabView>
|
||||
<div class="section-wrapper">
|
||||
<Section title="Status" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Pods ready</label>
|
||||
<span>{status?.readyReplicas}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Pods available</label>
|
||||
<span>{status?.availableReplicas}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Replicas</label>
|
||||
<span>{status?.replicas}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Metadata" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Namespace</label>
|
||||
<span>{metadata?.namespace}</span>
|
||||
</div>
|
||||
|
||||
{#if metadata?.ownerReferences?.length || 0 > 0}
|
||||
<div class="section-element">
|
||||
<label>Parent pod</label>
|
||||
<a
|
||||
href={`/cluster/${metadata?.ownerReferences?.[0].kind.toLowerCase()}/${
|
||||
metadata?.ownerReferences?.[0].uid
|
||||
}`}
|
||||
sveltekit:reload><span>{metadata?.ownerReferences?.[0].kind}</span></a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Spec" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Number of containers</label>
|
||||
<span>{spec?.template?.spec?.containers.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Scheduler</label>
|
||||
<span>{spec?.schedulerName}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Host network</label>
|
||||
<span>{spec?.hostNetwork ? 'yes' : 'no'}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>DNS policy</label>
|
||||
<span>{spec?.dnsPolicy}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.metadata} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.spec} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={deployment.status} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
@@ -1,39 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import type { PageData } from './$types';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
import Tab from '$lib/components/navigation/Tab.svelte';
|
||||
import Tabs from '$lib/components/navigation/Tabs.svelte';
|
||||
import TabList from '$lib/components/navigation/TabList.svelte';
|
||||
import TabView from '$lib/components/navigation/TabView.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import { formatDuration } from '$lib/utils/conversion';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Logs from '$lib/components/Logs.svelte';
|
||||
import JsonViewer from '$lib/components/JsonViewer.svelte';
|
||||
import { source } from 'sveltekit-sse';
|
||||
import type { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
const { pod }: { pod: V1Pod } = $props();
|
||||
const { status, metadata, spec } = pod || {};
|
||||
|
||||
let value = 'no data :(';
|
||||
if (browser) {
|
||||
console.log('setting up sse', window.location.pathname);
|
||||
value = source(window.location.pathname + '/logs').select('message');
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { pod }: { pod: V1Pod | undefined } = data;
|
||||
const { status, metadata, spec } = pod || {};
|
||||
// console.log(pod);
|
||||
|
||||
let uptime = writable(new Date().getTime() - new Date(status?.startTime || 0).getTime());
|
||||
|
||||
let logs = writable([]);
|
||||
let eventSource: EventSource;
|
||||
|
||||
const parentUrl = `/cluster/${metadata?.ownerReferences?.[0].kind.toLowerCase()}/${metadata?.ownerReferences?.[0].uid}`;
|
||||
|
||||
function setupWS() {
|
||||
const url = new URL(`${window.location.origin}/cluster/pod/${pod?.metadata?.uid}/logs`);
|
||||
if (pod?.metadata) {
|
||||
if (pod?.metadata === undefined) {
|
||||
console.error('missing pod info. not enough metadata to setup WS connection.');
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +48,13 @@
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => uptime.update((n) => n + 1000), 1000);
|
||||
return setupWS();
|
||||
|
||||
if (browser) {
|
||||
console.log('setting up sse', window.location.pathname);
|
||||
value = source(window.location.pathname + '/logs').select('message');
|
||||
console.log(value);
|
||||
return setupWS();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -65,13 +65,13 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader>Pod: {pod?.metadata?.name}</PageHeader>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Logs</Tab>
|
||||
<Tab>Metadata</Tab>
|
||||
<Tab>Spec</Tab>
|
||||
<Tab>Status</Tab>
|
||||
<Tab>Deployment logs</Tab>
|
||||
</TabList>
|
||||
|
||||
@@ -108,9 +108,11 @@
|
||||
<span>{metadata?.namespace}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Parent resource</label>
|
||||
<span>{metadata?.ownerReferences?.[0].kind}</span>
|
||||
<div class="section-element" data-sveltekit-preload-data="false">
|
||||
<label>Parent pod</label>
|
||||
<a href={parentUrl} sveltekit:reload
|
||||
><span>{metadata?.ownerReferences?.[0].kind}</span></a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -156,10 +158,18 @@
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs logs={JSON.stringify(metadata, null, 2).split('\n')} lineNumbers={false} />
|
||||
<JsonViewer json={pod.metadata} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs lineNumbers={false} />
|
||||
<JsonViewer json={pod.spec} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<JsonViewer json={pod.status} lineNumbers={false} />
|
||||
</TabView>
|
||||
|
||||
<TabView>
|
||||
<Logs logs="" lineNumbers={false} />
|
||||
</TabView>
|
||||
</Tabs>
|
||||
@@ -15,22 +15,6 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
letter-spacing: 0.2px;
|
||||
|
||||
&.selected {
|
||||
opacity: 1;
|
||||
letter-spacing: unset;
|
||||
border-bottom-color: var(--color) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
&:not(&:first-of-type) {
|
||||
margin-left: 0.75rem;
|
||||
@@ -40,10 +24,21 @@
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
opacity: 0.7;
|
||||
background: none;
|
||||
opacity: 0.6;
|
||||
margin: 0;
|
||||
padding-bottom: 0.3rem;
|
||||
transition: 0.3s ease-in-out all;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
letter-spacing: 0.2px;
|
||||
|
||||
&.selected {
|
||||
opacity: 1;
|
||||
letter-spacing: unset;
|
||||
border-bottom-color: var(--color) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
282
src/lib/components/prox-describe/VM.svelte
Normal file
@@ -0,0 +1,282 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { formatBytes, formatDuration } from '$lib/utils/conversion';
|
||||
import Cpu from '$lib/icons/cpu.svelte';
|
||||
import Section from '../Section.svelte';
|
||||
import type { Node, VM } from '$lib/interfaces/proxmox';
|
||||
import FloppyDisk from '$lib/icons/floppy-disk.svelte';
|
||||
import HardDisk from '$lib/icons/hard-disk.svelte';
|
||||
import Clock from '$lib/icons/clock.svelte';
|
||||
import InsertDown from '$lib/icons/insert-down.svelte';
|
||||
import ExtractUp from '$lib/icons/extract-up.svelte';
|
||||
import Table from '../Table.svelte';
|
||||
import Power from '$lib/icons/power.svelte';
|
||||
|
||||
const { vm, node }: { vm: VM, node: Node } = $props();
|
||||
console.log(node)
|
||||
|
||||
const drives = Object.entries(vm.config)
|
||||
.filter(([key, _]) => {
|
||||
return key.startsWith('scsi');
|
||||
})
|
||||
?.map(([key, value]) => {
|
||||
const { disk, backup, discard, iothread, size } =
|
||||
/(?<disk>[\w\:\-]+),(backup=(?<backup>[\w|\d]+))?[,]?(discard=(?<discard>[\w|\d]+))?[,]?(iothread=(?<iothread>[\w|\d]+))?[,]?(size=(?<size>[\w|\d]+)\w)?[,]?/.exec(
|
||||
value
|
||||
)?.groups || {};
|
||||
if (!disk) return;
|
||||
|
||||
return {
|
||||
device: key,
|
||||
mount: disk,
|
||||
backup: backup === '1' ? 'enabled' : 'disabled',
|
||||
size: Number(size || 1) * 1024
|
||||
};
|
||||
})
|
||||
.filter((d) => d)
|
||||
.sort((a, b) =>
|
||||
Number(a?.device.match(/\d+/)[0]) > Number(b?.device.match(/\d+/)[0]) ? 1 : -1
|
||||
);
|
||||
|
||||
const pcieDevices = Object.entries(vm.config)
|
||||
.filter(([key, _]) => {
|
||||
return key.startsWith('hostpci');
|
||||
})
|
||||
?.map(([key, value]) => {
|
||||
return {
|
||||
device: key,
|
||||
name: value
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Section title="Resources" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>ID</label>
|
||||
<span>{vm.vmid}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Memory usage</label>
|
||||
<span
|
||||
><span class="icon"><FloppyDisk /></span>{formatBytes(vm.mem).match(/\d+(\.\d+)?/)?.[0]} /
|
||||
{formatBytes(vm.maxmem)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>CPUs</label>
|
||||
<span>
|
||||
<span class="icon"><Cpu /></span>
|
||||
{vm.cpus} cores ({vm.config.sockets})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Disk</label>
|
||||
<span>
|
||||
<span class="icon"><HardDisk /></span>
|
||||
{formatBytes(vm.maxdisk)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Uptime</label>
|
||||
<span>
|
||||
<span class="icon"><Clock /></span>
|
||||
{formatDuration(vm.uptime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Status" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>State</label>
|
||||
<span>
|
||||
<span class="icon"><Power /></span>
|
||||
{vm.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>CPU load</label>
|
||||
<span>{Math.floor(vm.cpu * 10000) / 100} %</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Memory usage</label>
|
||||
<span>{Math.floor((vm.mem / vm.maxmem) * 1000) / 10} %</span>
|
||||
</div>
|
||||
<div class="section-element">
|
||||
<label>Netout</label>
|
||||
<span>
|
||||
<span class="icon"><ExtractUp /></span>
|
||||
{formatBytes(vm.netout)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Netin</label>
|
||||
<span>
|
||||
<span class="icon"><InsertDown /></span>
|
||||
{formatBytes(vm.netin)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Disk read</label>
|
||||
<span>
|
||||
<span class="icon"><InsertDown /></span>
|
||||
{vm.diskread}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Disk write</label>
|
||||
<span>
|
||||
<span class="icon"><InsertDown /></span>
|
||||
{vm.diskwrite}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Operating System" description="">
|
||||
<div class="section-row">
|
||||
<div class="section-element">
|
||||
<label>Name</label>
|
||||
<span>{vm.os?.name}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Name</label>
|
||||
<span>{vm.os?.['pretty-name']}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Version</label>
|
||||
<span>{vm.os?.version}</span>
|
||||
</div>
|
||||
|
||||
<div class="section-element">
|
||||
<label>Kernel</label>
|
||||
<span>{vm.os?.['kernel-release']}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Filesystem" description="">
|
||||
{#if drives?.length > 0}
|
||||
<Table title="FS Devices" columns={['name', 'bus', 'mountpoint', 'type', 'space', 'size']}>
|
||||
<tbody slot="tbody">
|
||||
{#each vm.fs as device (device)}
|
||||
<tr>
|
||||
<td>{device.name}</td>
|
||||
<td>
|
||||
{#each device['disk'] as device (device)}
|
||||
<span>{device?.['bus-type']}{device?.target}</span>
|
||||
{/each}
|
||||
</td>
|
||||
<td>{device.mountpoint}</td>
|
||||
<td>{device.type}</td>
|
||||
<td>{Math.floor((device['used-bytes'] / device['total-bytes']) * 10000) / 100} %</td>
|
||||
<td>{formatBytes(device['total-bytes'])}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<Section title="Devices" description="">
|
||||
{#if vm.network?.length > 0}
|
||||
<Table title="Network interfaces" columns={['name', 'hardward address', 'ip addresses']}>
|
||||
<tbody slot="tbody">
|
||||
{#each vm.network.sort((a,b) => a < b ? 1 : -1) as net_interface (net_interface.name)}
|
||||
<tr>
|
||||
<td>{net_interface.name}</td>
|
||||
<td>{net_interface['hardware-address']}</td>
|
||||
<td class="ip-addresses">
|
||||
{#each net_interface['ip-addresses'] as ip (ip['ip-address'])}
|
||||
<span>{ip['ip-address']}/{ip.prefix}</span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{/if}
|
||||
|
||||
{#if drives?.length > 0}
|
||||
<Table title="Hard drives" columns={['device', 'mount', 'backup', 'size']}>
|
||||
<div slot="actions">
|
||||
<p>Total drives: {drives.length}</p>
|
||||
<p>Total capacity: {formatBytes(drives.reduce((acc, obj) => acc + obj.size, 0))}</p>
|
||||
</div>
|
||||
|
||||
<tbody slot="tbody">
|
||||
{#each drives as drive (drive)}
|
||||
<tr>
|
||||
<td>{drive.device}</td>
|
||||
<td>{drive.mount}</td>
|
||||
<td>{drive.backup}</td>
|
||||
<td>{formatBytes(drive.size)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{/if}
|
||||
|
||||
{#if pcieDevices?.length > 0}
|
||||
<Table title="PCIe devices" columns={Object.keys(pcieDevices[0])}>
|
||||
<tbody slot="tbody">
|
||||
{#each pcieDevices as pcie (pcie)}
|
||||
<tr>
|
||||
<td>{pcie.device}</td>
|
||||
<td>{pcie.name}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
{/if}
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(article.main-container:not(:first-child)) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.section-element {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
--size: 1.3rem;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
padding-right: 0.5rem;
|
||||
|
||||
&.spin {
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
animation: rotate 6s linear infinite;
|
||||
transform-origin: calc((var(--size) / 2) - 2px) calc(var(--size) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody .ip-addresses {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
14
src/lib/icons/Length.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M752 512h-304v-64h48c8.8 0 16-7.2 16-16v-96c0-8.8-7.2-16-16-16h-57.2c-10.5-35.3-29.6-67.6-56.4-94.4-42.3-42.3-98.6-65.6-158.4-65.6s-116.1 23.3-158.4 65.6c-42.3 42.3-65.6 98.6-65.6 158.4v160c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64h288v64h32v-80c0-8.8-7.2-16-16-16zM480 352v64h-32v-32c0-10.8-0.8-21.5-2.3-32h34.3zM384 544h-320v-160c0-88.2 71.8-160 160-160s160 71.8 160 160v160z"
|
||||
></path>
|
||||
<path
|
||||
d="M224 288c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zM224 448c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64c0 35.3-28.7 64-64 64z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
30
src/lib/icons/barcore.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path d="M0 160h32v352h-32v-352z"></path>
|
||||
<path d="M64 160h64v352h-64v-352z"></path>
|
||||
<path d="M160 160h32v352h-32v-352z"></path>
|
||||
<path d="M224 160h32v352h-32v-352z"></path>
|
||||
<path d="M288 160h64v352h-64v-352z"></path>
|
||||
<path d="M736 160h32v352h-32v-352z"></path>
|
||||
<path d="M576 160h64v352h-64v-352z"></path>
|
||||
<path d="M672 160h32v352h-32v-352z"></path>
|
||||
<path d="M512 160h32v352h-32v-352z"></path>
|
||||
<path d="M448 160h32v352h-32v-352z"></path>
|
||||
<path d="M384 160h32v352h-32v-352z"></path>
|
||||
<path d="M0 544h32v64h-32v-64z"></path>
|
||||
<path d="M64 544h64v64h-64v-64z"></path>
|
||||
<path d="M160 544h32v64h-32v-64z"></path>
|
||||
<path d="M224 544h32v64h-32v-64z"></path>
|
||||
<path d="M288 544h64v64h-64v-64z"></path>
|
||||
<path d="M736 544h32v64h-32v-64z"></path>
|
||||
<path d="M576 544h64v64h-64v-64z"></path>
|
||||
<path d="M672 544h32v64h-32v-64z"></path>
|
||||
<path d="M512 544h32v64h-32v-64z"></path>
|
||||
<path d="M448 544h32v64h-32v-64z"></path>
|
||||
<path d="M384 544h32v64h-32v-64z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
src/lib/icons/bucket.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M671.9 418c0.6-9.2-2.8-18.1-9.3-24.6l-143.1-143.2c17.1-27.9 30.8-55.3 40.5-81.4 23.9-64.7 20.9-116.6-8.5-146.3-14.9-14.9-33.2-22.5-54.4-22.5v0c-31.1 0-68.8 16.6-115.2 50.8-10.9 8.1-22.4 17.2-34.5 27.3l-4.8-4.8c-6.5-6.5-15.5-9.9-24.6-9.3-9.2 0.6-17.6 5.1-23.3 12.3l-280.7 361.8c-19.7 25.4-17.4 61.7 5.3 84.5l194.1 194.1c12.4 12.4 28.8 18.7 45.3 18.7 13.8 0 27.6-4.4 39.2-13.4l361.7-280.7c7.3-5.7 11.8-14.1 12.3-23.3zM497.1 32v0c12.7 0 22.8 4.2 31.7 13.1 19.8 20 20.3 61 1.2 112.6-8.2 22.2-19.6 45.5-33.7 69.2l-126.3-126.1c47.6-39.6 93.5-68.8 127.1-68.8zM258.7 671.4l-194.1-194.1 30.6-39.5 202.9 202.9-39.4 30.7zM323.6 621l-208.6-208.6 208-268.1 136.8 136.7c-11.3 15.1-23.6 30.1-36.6 44.9-6.9-3.8-14.8-6-23.1-6-26.5 0-48 21.5-48 48s21.5 48 48 48c26.5 0 48-21.5 48-48 0-6.3-1.2-12.4-3.5-17.9 13.5-15.2 26.3-30.7 38.1-46.2l109 109.2-268.1 208zM416 368c0 8.8-7.2 16-16 16s-16-7.2-16-16 7.2-16 16-16 16 7.2 16 16z"></path>
|
||||
<path d="M730.3 584.5c-12.3-21.9-23.9-42.6-26.4-58.9-1.2-7.8-7.9-13.6-15.8-13.6s-14.6 5.8-15.8 13.6c-2.5 16.4-14.1 37-26.4 58.9-17.9 31.5-37.9 67.2-37.9 103.4 0 44.1 35.9 80 80 80s80-35.9 80-80c0-36.2-20-71.9-37.7-103.4zM688 735.9c-26.5 0-48-21.5-48-48 0-27.8 17.1-58.3 33.6-87.7 5-9 10-17.7 14.4-26.3 4.4 8.6 9.3 17.4 14.4 26.3 16.5 29.4 33.6 59.9 33.6 87.7 0 26.5-21.5 48-48 48z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
23
src/lib/icons/certificate.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path d="M192 128h160v32h-160v-32z"></path>
|
||||
<path d="M192 192h384v32h-384v-32z"></path>
|
||||
<path d="M192 256h352v32h-352v-32z"></path>
|
||||
<path d="M192 320h384v32h-384v-32z"></path>
|
||||
<path d="M192 384h128v32h-128v-32z"></path>
|
||||
<path d="M192 448h64v32h-64v-32z"></path>
|
||||
<path d="M192 512h96v32h-96v-32z"></path>
|
||||
<path
|
||||
d="M521 542.3c14.3-16.8 23-38.5 23-62.3 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 23.7 8.7 45.5 23 62.3l-85.3 170.6c-3.1 6.2-1.9 13.6 3 18.5s12.3 6.1 18.5 3l49.7-24.8 24.8 49.7c2.7 5.5 8.3 8.8 14.3 8.8 0.6 0 1.3 0 1.9-0.1 6.7-0.8 12.2-5.7 13.7-12.3l32.4-140.5 32.4 140.4c1.5 6.6 7 11.5 13.7 12.3 0.6 0.1 1.3 0.1 1.9 0.1 6 0 11.6-3.4 14.3-8.8l24.8-49.7 49.7 24.8c6.2 3.1 13.6 1.9 18.5-3s6.1-12.3 3-18.5l-85.3-170.5zM448 448c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32zM394.4 705.1l-12.1-24.2c-4-7.9-13.6-11.1-21.5-7.2l-21.1 10.5 60.5-121c7.6 4.4 15.9 7.7 24.6 9.9l-30.4 132zM535.2 673.7c-7.9-4-17.5-0.7-21.5 7.2l-12.1 24.2-30.4-131.9c8.7-2.2 17-5.5 24.6-9.9l60.5 121-21.1-10.6z"
|
||||
></path>
|
||||
<path
|
||||
d="M608 0h-448c-35.3 0-64 28.7-64 64v544c0 35.3 28.7 64 64 64h128v-64h-128v-544h448v544c0 0 0 0 0 0v64c35.3 0 64-28.7 64-64v-544c0-35.3-28.7-64-64-64z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
src/lib/icons/color-sampler.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M480 160c-35.3 0-64 28.7-64 64s28.7 64 64 64 64-28.7 64-64-28.7-64-64-64zM480 256c-17.6 0-32-14.4-32-32s14.4-32 32-32 32 14.4 32 32-14.4 32-32 32z"></path>
|
||||
<path d="M764.8 390.4l-103.6-138.1-19.1-64.8c-0.5-1.7-1.2-3.2-2.1-4.5v-55c0-35.3-28.7-64-64-64h-512c-35.3 0-64 28.7-64 64v192c0 35.3 28.7 64 64 64h11.5l53.1 180.5c2.1 7 8.4 11.5 15.3 11.5 1.5 0 3-0.2 4.5-0.7l94.2-27.7 112.5 150c3.1 4.2 7.9 6.4 12.8 6.4 3.3 0 6.7-1 9.6-3.2l384-288c7.2-5.3 8.6-15.3 3.3-22.4zM64 128c0 0 0 0 0 0h512v192h-512v-192zM108.9 384h467.1c35.3 0 64-28.7 64-64v-26.5l28.1 95.6-513.3 151-45.9-156.1zM371.2 665.6l-95.7-127.6 417-122.7c4.1-1.2 7.5-4 9.5-7.7s2.5-8.1 1.3-12.2l-16.3-55.3 42.6 56.7-358.4 268.8z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -2,8 +2,8 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
|
||||
|
Before Width: | Height: | Size: 816 B After Width: | Height: | Size: 818 B |
@@ -1,12 +1,10 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M701.2 186.6c0 0 0 0 0 0l-288-147.6c-18.3-9.3-40.1-9.3-58.4 0l-288 147.6c-21.4 11-34.8 32.9-34.8 57v280.9c0 24.1 13.3 45.9 34.8 57l288 147.6c9.1 4.7 19.1 7 29.1 7s20.1-2.3 29.2-7l288-147.6c21.4-11 34.8-32.9 34.8-57v-280.9c0.1-24.1-13.2-45.9-34.7-57zM384 96l249.8 128-249.8 128-249.8-128 249.8-128zM96 276.4l256 131.2v248.2l-256-131.3v-248.1zM416 655.7v-248.1l256-131.2v248.2l-256 131.1z"
|
||||
></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 805 B |
4
src/lib/icons/desktop.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
14
src/lib/icons/external.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M736 0h-256v64h178.7l-401.3 401.4 45.3 45.3 401.3-401.4v178.7h64v-256c0-17.7-14.3-32-32-32z"
|
||||
></path>
|
||||
<path
|
||||
d="M544 736h-512v-512h256v-32h-272c-8.8 0-16 7.2-16 16v544c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-272h-32v256z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
5
src/lib/icons/extract-up.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M656 352h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
|
||||
<path d="M352 141.3v402.7h64v-402.7l81.4 81.4 45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.3-81.4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 455 B |
15
src/lib/icons/fingerprint.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M672 368c0-6.4-0.3-12.7-0.9-19-4.6-92.3-27.6-171.9-67.2-231.6-50.9-76.8-127-117.4-219.9-117.4s-169 40.6-219.9 117.4c-44.6 67.2-68.1 159.3-68.1 266.6 0 88.7 5.7 189.6 48.1 264.6 21.9 38.7 52.4 68.4 90.7 88.1 40.7 21 89.5 31.2 149.2 31.2s108.5-10.2 149.2-31.2c38.3-19.8 68.8-49.4 90.7-88.1 42.4-75 48.1-175.9 48.1-264.6 0-5.4-0.1-10.7-0.2-16h0.2zM217.4 152.8c39.1-58.9 95.1-88.8 166.6-88.8 44.3 0 82.7 11.5 114.7 34.2-11.5-1.4-23.1-2.2-34.7-2.2-50 0-121.8 9.2-179.7 53.3-61.2 46.5-92.3 120.1-92.3 218.7 0 79.4 64.6 144 144 144s144-64.6 144-144v-16h-32v16c0 61.8-50.2 112-112 112s-112-50.2-112-112c0-208.8 150.4-240 240-240 26.6 0 53.2 4.5 78.3 13.1 2.8 3.8 5.6 7.6 8.3 11.7 7.6 11.4 14.4 23.7 20.5 37-31.3-18.9-68-29.8-107.1-29.8-114.7 0-208 93.3-208 208 0 44.1 35.9 80 80 80s80-35.9 80-80c0-26.5 21.5-48 48-48s48 21.5 48 48c0 97-79 176-176 176-95.2 0-173-76-175.9-170.6 1.4-90 21.2-166.1 57.3-220.6zM568.1 617.2c-33.5 59.2-92 86.8-184.1 86.8s-150.7-27.6-184.1-86.8c-1.3-2.3-2.5-4.6-3.7-6.9 37.9 19.4 80.1 29.7 123.8 29.7v-32c-51.8 0-101.4-16.5-142.5-46.8-6.6-24-10.8-49.8-13.4-76.2 37.5 54.9 100.5 91 171.9 91 114.7 0 208-93.3 208-208 0-44.1-35.9-80-80-80s-80 35.9-80 80c0 26.5-21.5 48-48 48s-48-21.5-48-48c0-97 79-176 176-176 50.1 0 95.4 21 127.5 54.8 10.9 40.7 16.5 86.9 16.5 137.2 0 80.4-4.7 171-39.9 233.2z"
|
||||
></path>
|
||||
<path d="M320 368v16h32v-16c0-61.8 50.2-112 112-112h16v-32h-16c-79.4 0-144 64.6-144 144z"></path>
|
||||
<path
|
||||
d="M512 224v32c15.1 0 30.6 12.4 42.6 34 13.8 24.9 21.4 58.3 21.4 94 0 123.3-70.1 256-224 256v32c80.1 0 147.1-32 193.7-92.5 20.4-26.5 36.2-57.6 47-92.3 10.2-33 15.3-67.6 15.3-103.1 0-89.8-42.2-160.1-96-160.1z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,108.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 896 B After Width: | Height: | Size: 879 B |
@@ -1,12 +1,10 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M672 32h-576c-35.3 0-64 28.7-64 64v498.7c0 17.1 6.7 33.1 18.8 45.2l77.3 77.3c12.1 12.1 28.2 18.8 45.2 18.8h498.7c35.3 0 64-28.7 64-64v-576c0-35.3-28.7-64-64-64zM192 64h384v256c0 17.6-14.4 32-32 32h-320c-17.6 0-32-14.4-32-32v-256zM544 704h-288v-192h288v192zM672 672h-96v-176c0-8.8-7.2-16-16-16h-320c-8.8 0-16 7.2-16 16v176h-50.7l-77.3-77.3v-498.7h64v224c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64v-224h64v576z"
|
||||
></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 760 B After Width: | Height: | Size: 699 B |
13
src/lib/icons/flower.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M768.2 384c0-57.1-37.6-105.6-89.3-122 9.4-18 14.4-38.2 14.4-59.1 0-34.2-13.3-66.3-37.5-90.5s-56.3-37.5-90.5-37.5c-20.9 0-41 5-59.1 14.4-16.4-51.7-64.9-89.3-122-89.3-57.2-0.1-105.7 37.5-122.2 89.3-48.2-25-109.2-17.3-149.6 23.1s-48.1 101.4-23.1 149.6c-51.7 16.4-89.3 64.9-89.3 122s37.6 105.6 89.4 122.1c-25 48.2-17.3 109.2 23.1 149.6s101.3 48.1 149.6 23.1c16.4 51.8 64.9 89.4 122.1 89.4 57.1 0 105.6-37.6 122-89.3 18 9.4 38.2 14.4 59.1 14.4 34.2 0 66.3-13.3 90.5-37.5s37.5-56.3 37.5-90.5c0-20.9-5-41-14.4-59.1 51.7-16.6 89.3-65.1 89.3-122.2zM506.7 167.9c0.6-0.4 1.3-0.9 1.8-1.2 3.1-2.1 7.4-5 11.4-9 25-25 65.6-25 90.5 0 25 25 25 65.6 0 90.5-4 4-6.7 8-8.9 11.3-0.4 0.6-1 1.4-1.4 2-6.3 5.2-10.1 12.5-11.2 20.3l-93.8 38.9c-11.4-19.9-27.9-36.5-47.8-47.8l38.9-93.7c7.9-1.1 15.2-5 20.5-11.3zM384 448c-35.3 0-64-28.7-64-64s28.7-64 64-64c35.3 0 64 28.7 64 64s-28.7 64-64 64zM318.4 142.4c0.7-3.7 1.7-8.8 1.7-14.5 0-35.3 28.7-64 64-64s64 28.7 64 64c0 5.7 1 10.7 1.7 14.4 0.1 0.7 0.3 1.6 0.4 2.3-0.7 8.1 1.7 16 6.4 22.3l-38.9 93.7c-10.8-3-22.1-4.5-33.8-4.5s-23 1.6-33.8 4.5l-38.7-93.6c4.8-6.3 7.3-14.2 6.6-22.4 0.1-0.7 0.3-1.5 0.4-2.2zM157.7 157.6c25-25 65.6-25 90.5 0 4 4 8 6.7 11.3 8.9 0.6 0.4 1.4 1 2 1.4 5.2 6.2 12.5 10.1 20.2 11.2l38.9 93.7c-19.9 11.4-36.5 27.9-47.8 47.8l-93.6-38.8c-1-7.8-4.9-15.2-11.2-20.4-0.4-0.6-0.9-1.2-1.2-1.8-2.1-3.1-5-7.4-9-11.4-25-25-25-65.6-0.1-90.6zM64 384c0-35.3 28.7-64 64-64 5.7 0 10.7-1 14.4-1.7 0.7-0.1 1.6-0.3 2.3-0.4 8.1 0.7 16.1-1.7 22.3-6.5l93.6 38.8c-3 10.8-4.5 22.1-4.5 33.8s1.6 23 4.5 33.8l-93.6 38.8c-6.2-4.8-14.1-7.2-22.3-6.5-0.7-0.1-1.5-0.3-2.2-0.4-3.7-0.7-8.8-1.7-14.5-1.7-35.3 0-64-28.7-64-64zM261.5 600.1c-0.6 0.4-1.2 0.9-1.8 1.2-3.1 2.1-7.4 5-11.4 9-25 25-65.6 25-90.5 0-25-25-25-65.6 0-90.5 4-4 6.7-8 8.9-11.3 0.4-0.6 1-1.4 1.4-2 6.3-5.2 10.1-12.6 11.2-20.4l93.6-38.8c11.4 19.9 27.9 36.5 47.8 47.8l-38.9 93.7c-7.8 1.2-15.1 5.1-20.3 11.3zM449.8 625.6c-0.7 3.7-1.7 8.8-1.7 14.5 0 35.3-28.7 64-64 64s-64-28.7-64-64c0-5.7-1-10.7-1.7-14.4-0.1-0.7-0.3-1.6-0.4-2.3 0.7-8.2-1.7-16.2-6.6-22.4l38.8-93.6c10.8 3 22.1 4.5 33.8 4.5s23-1.6 33.8-4.5l38.9 93.7c-4.7 6.2-7.2 14.1-6.5 22.2-0.1 0.8-0.3 1.6-0.4 2.3zM610.5 610.4c-25 25-65.6 25-90.5 0-4-4-8-6.7-11.3-8.9-0.6-0.4-1.4-1-2-1.4-5.3-6.3-12.6-10.2-20.4-11.2l-38.9-93.7c19.9-11.4 36.5-27.9 47.8-47.8l93.8 38.9c1.1 7.8 4.9 15.1 11.2 20.3 0.4 0.6 0.9 1.3 1.2 1.8 2.1 3.1 5 7.4 9 11.4 25 25 25 65.6 0.1 90.6zM640.2 448c-5.7 0-10.7 1-14.4 1.7-0.7 0.1-1.6 0.3-2.3 0.4-8.2-0.7-16.1 1.7-22.4 6.5l-93.7-38.9c3-10.8 4.5-22.1 4.5-33.8s-1.6-23-4.5-33.8l93.7-38.9c6.2 4.8 14.2 7.3 22.3 6.5 0.7 0.1 1.5 0.3 2.2 0.4 3.7 0.7 8.8 1.7 14.5 1.7 35.3 0 64 28.7 64 64 0.1 35.5-28.6 64.2-63.9 64.2z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,12 +1,10 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M640 0h-512c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM640 704h-512v-640h512v640c0 0 0 0 0 0z"
|
||||
></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
14
src/lib/icons/helm.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M384 256c-70.6 0-128 57.4-128 128s57.4 128 128 128c70.6 0 128-57.4 128-128s-57.4-128-128-128zM384 448c-35.3 0-64-28.7-64-64s28.7-64 64-64c35.3 0 64 28.7 64 64s-28.7 64-64 64z"
|
||||
></path>
|
||||
<path
|
||||
d="M768 416v-64h-97.7c-6-54.3-27.1-105.2-61.2-147.8l69.1-69.1-45.3-45.3-69.1 69.2c-42.6-34.2-93.5-55.3-147.8-61.2v-97.8h-64v97.7c-54.3 6-105.2 27.1-147.8 61.2l-69.1-69.1-45.3 45.3 69.1 69.1c-34.2 42.6-55.3 93.5-61.2 147.8h-97.7v64h97.7c6 54.3 27.1 105.2 61.2 147.8l-69.1 69.1 45.3 45.3 69.1-69.1c42.6 34.2 93.5 55.3 147.8 61.2v97.7h64v-97.7c54.3-6 105.2-27.1 147.8-61.2l69.1 69.1 45.3-45.3-69.2-69.1c34.2-42.6 55.3-93.5 61.2-147.8h97.8zM638 352h-64.7c-4.9-29.1-16.4-56.1-32.9-79.2l45.7-45.7c27.6 35.4 46.1 78.2 51.9 124.9zM384 544c-88.2 0-160-71.8-160-160s71.8-160 160-160c88.2 0 160 71.8 160 160s-71.8 160-160 160zM540.9 181.8l-45.7 45.7c-23.1-16.5-50.1-28-79.2-32.9v-64.6c46.7 5.8 89.5 24.3 124.9 51.8zM352 130v64.7c-29.1 4.9-56.1 16.4-79.2 32.9l-45.7-45.7c35.4-27.6 78.2-46.1 124.9-51.9zM181.8 227.1l45.7 45.7c-16.5 23.1-28 50.1-32.9 79.2h-64.6c5.8-46.7 24.3-89.5 51.8-124.9zM130 416h64.7c4.9 29.1 16.4 56.1 32.9 79.2l-45.7 45.7c-27.6-35.4-46.1-78.2-51.9-124.9zM227.1 586.2l45.7-45.7c23.1 16.5 50.1 28 79.2 32.9v64.6c-46.7-5.8-89.5-24.3-124.9-51.8zM416 638v-64.7c29.1-4.9 56.1-16.4 79.2-32.9l45.7 45.7c-35.4 27.6-78.2 46.1-124.9 51.9zM586.2 540.9l-45.7-45.7c16.5-23.1 28-50.1 32.9-79.2h64.6c-5.8 46.7-24.3 89.5-51.8 124.9z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
src/lib/icons/id.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" height="100%" width="100%">
|
||||
<path d="M704 128h-640c-35.3 0-64 28.7-64 64v384c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-384c0-35.3-28.7-64-64-64zM704 576h-640v-384h640v384c0.1 0 0 0 0 0z"></path>
|
||||
<path d="M416 256h256v32h-256v-32z"></path>
|
||||
<path d="M416 320h192v32h-192v-32z"></path>
|
||||
<path d="M416 384h256v32h-256v-32z"></path>
|
||||
<path d="M416 448h256v32h-256v-32z"></path>
|
||||
<path d="M298.3 402.5c-4.7-2.8-9.6-5.2-14.6-7.4 22.1-17.6 36.3-44.7 36.3-75.1 0-52.9-43.1-96-96-96s-96 43.1-96 96c0 30.4 14.2 57.5 36.3 75.1-5 2.1-9.9 4.6-14.6 7.4-20.2 11.9-37 29.3-51.4 53.3-4.5 7.4-2.2 17.1 5.1 21.8 36.6 23.1 76.1 34.4 120.6 34.4s84-11.3 120.6-34.5c7.3-4.7 9.6-14.3 5.1-21.8-14.4-23.9-31.2-41.3-51.4-53.2zM160 320c0-35.3 28.7-64 64-64s64 28.7 64 64-28.7 64-64 64-64-28.7-64-64zM224 480c-32.6 0-61.9-7-89.2-21.3 22.1-29.4 50.2-42.7 89.2-42.7s67.1 13.3 89.2 42.7c-27.3 14.3-56.6 21.3-89.2 21.3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
src/lib/icons/insert-down.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M656 320h-208v32h192v320h-512v-320h192v-32h-208c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
|
||||
<path d="M270.6 417.4l-45.3 45.3 136 136c6.2 6.2 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4l136-136-45.3-45.3-81.2 81.3v-434.7h-64v434.7l-81.4-81.3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
5
src/lib/icons/insert-up.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M656 64h-544c-8.8 0-16 7.2-16 16v352c0 8.8 7.2 16 16 16h208v-32h-192v-320h512v320h-192v32h208c8.8 0 16-7.2 16-16v-352c0-8.8-7.2-16-16-16z"></path>
|
||||
<path d="M497.4 350.6l45.3-45.3-136-136c-12.5-12.5-32.8-12.5-45.3 0l-136 136 45.3 45.3 81.4-81.4v434.8h64v-434.7l81.3 81.3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -2,8 +2,8 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
|
||||
|
Before Width: | Height: | Size: 852 B After Width: | Height: | Size: 854 B |
5
src/lib/icons/paint-roller.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M672 96h-32v-32c0-35.3-28.7-64-64-64h-416c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64h416c35.3 0 64-28.7 64-64v-32h32c17.6 0 32 14.4 32 32v96c0 17.6-14.4 32-32 32h-256c-35.3 0-64 28.7-64 64v1.6c-36.5 7.4-64 39.8-64 78.4v256c0 44.1 35.9 80 80 80s80-35.9 80-80v-256c0-38.6-27.5-71-64-78.4v-1.6c0-17.6 14.4-32 32-32h256c35.3 0 64-28.7 64-64v-96c0-35.3-28.7-64-64-64zM128 160v-96c0-17.6 14.4-32 32-32h416c17.6 0 32 14.4 32 32v32c0 17.6-14.4 32-32 32-14.2 0-21-7.9-32.8-23.1-13.4-17.3-31.7-40.9-71.2-40.9-33.3 0-49.3 20.6-62.2 37.2-13.3 17.1-21.8 26.8-41.8 26.8-26.8 0-45.1-9-62.8-17.6-21.2-10.4-43.1-21.1-71.4-9.1-18.1 7.6-29.4 16.4-35.7 27.5-6.1 10.7-6.1 21.4-6.1 30v1.2c0 17.6-14.4 32-32 32s-32-14.4-32-32zM224 160v-1.3c0-6.9 0.1-11 1.9-14.2 2.5-4.5 9.6-9.2 20.3-13.8 14.3-6 24.7-1.6 44.9 8.4 19 9.3 42.6 20.9 76.9 20.9 36.7 0 53.5-21.7 67.1-39.2 12.7-16.3 20.1-24.8 36.9-24.8 23.8 0 33.6 12.6 45.9 28.5 12.3 15.8 27.6 35.5 58.1 35.5h-352zM384 688c0 8.8-7.2 16-16 16s-16-7.2-16-16v-256c0-8.8 7.2-16 16-16s16 7.2 16 16v256z"></path>
|
||||
<path d="M159.9 301.8c-1.1-7.9-7.9-13.8-15.9-13.8s-14.8 5.9-15.9 13.8c-1.2 8.4-6.7 19.8-12.7 31.9-9.1 18.6-19.5 39.7-19.5 61.7 0 29 21.5 52.6 48 52.6s48-23.6 48-52.6c0-22-10.3-43.1-19.5-61.7-5.8-12.1-11.4-23.4-12.5-31.9zM144 416c-8.7 0-16-9.4-16-20.6 0-14.4 8.1-31.1 16-47.2 7.9 16.1 16 32.7 16 47.2 0 11.2-7.3 20.6-16 20.6z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
src/lib/icons/palette.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M716.8 352.6c-25.9-18.5-57.8-29.9-85.9-40-15.3-5.5-29.8-10.7-40.8-16.1-9.8-4.8-13.1-8.1-14.1-9.3 0-1.9 0.3-2.9 0.4-3.4 2.7-2.2 12.2-5.2 18-7 27.1-8.6 77.6-24.6 77.6-100.8 0-45.8-29.3-85.2-82.5-111.1-43.8-21.3-103.3-33-167.4-33-102.3 0-203.8 29.1-278.4 79.8-94 63.9-143.7 158-143.7 272.3 0 51.2 9.4 99.5 28 143.5 18.1 42.8 44.4 80.4 78.1 111.9 33.2 31 72.7 55.1 117.2 71.6 44.8 16.6 93.4 25 144.6 25 50.4 0 99.9-6.7 147.1-20 47.2-13.2 89.7-32.3 126.5-56.8 81.6-54 126.5-129 126.5-211.2 0-39-17.2-71-51.2-95.4zM606.3 605.9c-63.2 42-150.1 66.1-238.3 66.1-179 0-304-118.4-304-288 0-93.4 38.9-167.2 115.6-219.4 64.3-43.7 152.7-68.8 242.5-68.8 104.2 0.1 185.9 35.3 185.9 80.2 0 29.4-8.3 32-32.9 39.8-12.4 3.9-26.5 8.4-38.6 18-11.2 8.9-24.5 25.5-24.5 54.1 0 24.2 12.9 44.3 38.3 59.7 16.7 10.1 37.3 17.4 59 25.2 57.2 20.4 94.7 36.7 94.7 75.1 0 60.1-34.7 116.2-97.7 158z"></path>
|
||||
<path d="M224 400c0-26.5-21.5-48-48-48s-48 21.5-48 48c0 26.5 21.5 48 48 48s48-21.5 48-48zM176 416c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
|
||||
<path d="M240 480c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zM240 544c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
|
||||
<path d="M240 224c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zM240 288c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
|
||||
<path d="M368 160c-26.5 0-48 21.5-48 48s21.5 48 48 48c26.5 0 48-21.5 48-48s-21.5-48-48-48zM368 224c-8.8 0-16-7.2-16-16s7.2-16 16-16 16 7.2 16 16-7.2 16-16 16z"></path>
|
||||
<path d="M542.1 417c-23.2-3.2-48.3 1.4-70.7 12.9-27.1 13.9-46.4 36.1-53 60.7-4.9 18.2-2.3 36.5 7.3 51.5 11.4 17.9 31.4 29.6 56.1 32.9 4.8 0.7 9.7 1 14.6 1 18.9 0 38.3-4.8 56.1-13.9 27.1-13.9 46.4-36.1 53-60.7 4.9-18.2 2.3-36.5-7.3-51.5-11.4-17.9-31.4-29.6-56.1-32.9zM574.7 493.1c-4.2 15.7-18 30.9-36.8 40.6-32.4 16.7-71.4 12.6-85.2-8.8-6-9.4-5.2-19.3-3.4-25.9 4.2-15.7 18-30.9 36.8-40.6 13.5-6.9 28.1-10.3 41.6-10.3 18.9 0 35.6 6.6 43.6 19.1 6 9.4 5.2 19.3 3.4 25.9z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
17
src/lib/icons/pencil-ruler.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M748.2 2.4c-12-5-25.7-2.2-34.9 6.9l-704 704c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h672c35.3 0 64-28.7 64-64v-672c0-12.9-7.8-24.6-19.8-29.6zM640 704v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-64v-32h-32v32h-114.7l594.7-594.7v114.7h-32v32h32v64h-32v32h32v64h-32v32h32v64h-32v32h32v64h-32v32h32v64h-64z"
|
||||
></path>
|
||||
<path
|
||||
d="M598.1 385.2c-6-2.5-12.9-1.1-17.4 3.5l-192 192c-4.6 4.6-5.9 11.5-3.5 17.4s8.3 9.9 14.8 9.9h192c8.8 0 16-7.2 16-16v-192c0-6.5-3.9-12.3-9.9-14.8zM576 576h-137.4l137.4-137.4v137.4z"
|
||||
></path>
|
||||
<path
|
||||
d="M16 480c1.3 0 2.6-0.2 3.9-0.5l128-32c2.8-0.7 5.4-2.2 7.4-4.2l300-300c32.7-32.7 32.7-85.9 0-118.6s-85.9-32.7-118.6 0l-300 300c-2.1 2.1-3.5 4.6-4.2 7.4l-32 128c-1.4 5.5 0.2 11.2 4.2 15.2 3 3.1 7.1 4.7 11.3 4.7zM432.7 47.3c20.2 20.2 20.2 53.1 0 73.4l-16.7 16.7-73.4-73.4 16.7-16.7c20.2-20.2 53.2-20.2 73.4 0zM62.4 344.2l257.6-257.6 73.4 73.4-257.6 257.6-97.8 24.4 24.4-97.8z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
src/lib/icons/picture.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M704 0h-640c-35.3 0-64 28.7-64 64v640c0 35.3 28.7 64 64 64h640c35.3 0 64-28.7 64-64v-640c0-35.3-28.7-64-64-64zM704 704h-640l-0.1-640c0 0 0 0 0.1 0h640v640z"
|
||||
></path>
|
||||
<path
|
||||
d="M112 544h544c8.8 0 16-7.2 16-16v-416c0-8.8-7.2-16-16-16h-544c-8.8 0-16 7.2-16 16v416c0 8.8 7.2 16 16 16zM338.7 512l214.7-214.7c12.4-12.4 32.8-12.4 45.3 0l41.3 41.3v173.4h-301.3zM640 128v165.4l-18.7-18.7c-25-24.9-65.6-24.9-90.5 0l-237.3 237.3h-146.8l150.7-150.7c12.4-12.4 32.8-12.4 45.3 0l29.3 29.3 22.6-22.6-29.3-29.3c-25-24.9-65.6-24.9-90.5 0l-146.8 146.8v-357.5h512z"
|
||||
></path>
|
||||
<path
|
||||
d="M224 288c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zM224 192c17.6 0 32 14.4 32 32s-14.4 32-32 32-32-14.4-32-32 14.4-32 32-32z"
|
||||
></path>
|
||||
<path d="M256 608h256v32h-256v-32z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 903 B |
@@ -2,8 +2,8 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="768"
|
||||
height="768"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
|
||||
|
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 649 B |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,157.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,151.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,156.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
4
src/lib/icons/quill.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
|
||||
<path d="M22.866 1.503c-0.178-0.312-0.506-0.503-0.866-0.503-2.319 0-4.803 0.503-7.178 1.45-2.463 0.984-4.803 2.45-6.769 4.237-2.163 1.969-3.866 4.294-5.066 6.906-1.319 2.881-1.987 6.047-1.987 9.406h2c0-2.288 0.328-4.319 0.869-6.113 1.131-1.9 2.109-1.906 3.856-1.919 1.644-0.012 3.687-0.028 6.244-1.688 2.822-1.831 5.731-5.359 8.894-10.778 0.181-0.309 0.184-0.691 0.003-1zM18.225 6.041c-1.266 0.112-3.844 0.525-6.012 2.050l-0.409 0.288 0.575 0.819 0.409-0.288c1.534-1.078 3.341-1.547 4.609-1.753-1.572 2.037-3.069 3.506-4.516 4.447-2.066 1.341-3.644 1.353-5.169 1.366-0.756 0.006-1.522 0.012-2.275 0.2 1.181-2.137 2.631-3.784 3.966-5 3.056-2.784 6.972-4.603 10.766-5.056-0.663 1.066-1.309 2.044-1.944 2.928z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 881 B |
8
src/lib/icons/search.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg height="100%" width="100%" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path
|
||||
fill="inherit"
|
||||
fill-rule="evenodd"
|
||||
d="M10 2a8 8 0 1 0 4.24 14.785l4.687 4.688a1.8 1.8 0 0 0 2.546-2.546l-4.688-4.687A8 8 0 0 0 10 2m-6.2 8a6.2 6.2 0 1 1 12.4 0 6.2 6.2 0 0 1-12.4 0"
|
||||
clip-rule="evenodd"
|
||||
></path></svg
|
||||
>
|
||||
|
After Width: | Height: | Size: 306 B |
17
src/lib/icons/speed.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="90%"
|
||||
height="90%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M345.9 499.3c11.5 8.6 25 12.7 38.3 12.7 19.6 0 38.9-8.9 51.5-25.7 0 0 0-0.1 0.1-0.1 2.7-3.6 4.9-7.3 6.5-11l66.7-141.5c6.5-13.7 2.4-30.2-9.8-39.2-12.1-9.1-29.1-8.4-40.5 1.7l-117.2 104.1c0 0-0.1 0.1-0.1 0.1-3 2.7-5.8 5.8-8.4 9.1 0 0.1-0.1 0.1-0.1 0.2-10.3 13.7-14.6 30.6-12.1 47.5 2.4 17 11.3 32 25.1 42.1zM358.3 429.1c1.4-1.8 2.9-3.4 4.5-4.9l117.1-104.2c0 0 0 0 0 0l-66.7 141.6-0.1 0.3c-0.8 1.7-1.8 3.5-3.2 5.3-10.6 14.1-30.7 17-45 6.4-6.9-5.1-11.3-12.5-12.6-20.9-1.2-8.4 0.9-16.8 6-23.6z"
|
||||
></path>
|
||||
<path d="M256 576h256v32h-256v-32z"></path>
|
||||
<path
|
||||
d="M737.8 298.5c-19.3-45.7-47-86.8-82.3-122.1s-76.3-62.9-122-82.3c-47.4-19.9-97.7-30.1-149.5-30.1s-102.1 10.2-149.5 30.2c-45.7 19.3-86.8 47-122 82.3s-62.9 76.3-82.3 122.1c-20 47.3-30.2 97.6-30.2 149.4 0 45.4 7.8 89.8 23.3 132.1 14.9 40.8 36.6 78.6 64.4 112.3 6.1 7.4 15.1 11.6 24.7 11.6h543.2c9.6 0 18.6-4.3 24.7-11.6 27.8-33.7 49.4-71.5 64.4-112.3 15.5-42.3 23.3-86.7 23.3-132.1 0-51.8-10.2-102.1-30.2-149.5zM640.1 640h-512.2c-34.9-46.6-56.5-102.2-62.3-160h62.4v-64h-62.4c6.4-64.3 31.9-123 70.7-170.4l41 41 45.3-45.3-41-41c47.4-38.8 106.1-64.3 170.3-70.7v62.4h64v-62.4c64.3 6.4 123.1 31.9 170.5 70.8l-41 41 45.3 45.3 41-41c38.8 47.3 64.3 106 70.7 170.3h-62.4v64h62.4c-5.8 57.8-27.4 113.4-62.3 160z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,122.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 729 B |
15
src/lib/icons/sync.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 2c2.703 0 5.272 1.1 7.141 3h-4.141v2h6c0.553 0 1-0.447 1-1v-6h-2v3.059c-0.725-0.647-1.525-1.209-2.384-1.666-1.716-0.912-3.659-1.394-5.616-1.394-1.619 0-3.191 0.319-4.672 0.944-1.428 0.603-2.712 1.469-3.813 2.572s-1.966 2.384-2.572 3.813c-0.625 1.481-0.944 3.053-0.944 4.672h2c0-5.512 4.488-10 10-10z"
|
||||
></path>
|
||||
<path
|
||||
d="M22 12c0 5.513-4.488 10-10 10-2.703 0-5.272-1.1-7.141-3h4.141v-2h-6c-0.553 0-1 0.447-1 1v6h2v-3.059c0.725 0.647 1.525 1.209 2.384 1.666 1.719 0.912 3.659 1.394 5.616 1.394 1.619 0 3.191-0.319 4.672-0.944 1.428-0.603 2.712-1.469 3.813-2.572s1.966-2.384 2.572-3.813c0.625-1.481 0.944-3.053 0.944-4.672h-2z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 802 B |
6
src/lib/icons/tag.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
|
||||
<path d="M24 5c0-2.756-2.244-5-5-5-2.416 0-4.434 1.722-4.9 4h-2.091c-0.534 0-1.034 0.209-1.413 0.588l-10 10c-0.781 0.781-0.781 2.050 0 2.831l5.991 5.988c0.391 0.391 0.903 0.584 1.416 0.584s1.025-0.194 1.416-0.584l10-10c0.378-0.378 0.588-0.881 0.588-1.413v-2.094c2.275-0.466 3.994-2.487 3.994-4.9zM16 9v0c0 0.55-0.45 1-1 1s-1-0.45-1-1 0.45-1 1-1c0 0 0 0 0 0 0.281 0.375 0.616 0.712 1 1zM18.003 11.994l-10 10-5.994-5.994 10-10h2.091c0.075 0.372 0.194 0.734 0.35 1.075-0.837 0.237-1.453 1.009-1.453 1.925 0 1.103 0.897 2 2 2s2-0.897 2-2c0-0.897-0.594-1.659-1.413-1.913-0.206-0.337-0.363-0.703-0.462-1.088h2.878c0.003 0 0.003 0.003 0.003 0.006v5.988zM20.003 8.872v-2.878c-0.006-1.1-0.906-1.994-2.003-1.994h-2.875c0.444-1.722 2.013-3 3.875-3 2.206 0 4 1.794 4 4 0 1.859-1.275 3.425-2.997 3.872z"></path>
|
||||
<path d="M4.793 16.5l4.707-4.707 0.707 0.707-4.707 4.707-0.707-0.707z"></path>
|
||||
<path d="M6.794 18.499l5.708-5.708 0.707 0.707-5.708 5.708-0.707-0.707z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,182.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -8,7 +8,6 @@
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,181.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
5
src/lib/icons/text-color.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M64 736h640v32h-640v-32z"></path>
|
||||
<path d="M245.5 416h276.9l96.4 213.2 58.3-26.4-264-584c-5.1-11.4-16.5-18.8-29.1-18.8s-24 7.4-29.2 18.8l-264 584 58.3 26.4 96.4-213.2zM384 109.7l109.5 242.3h-219l109.5-242.3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 393 B |
5
src/lib/icons/text-size.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M768 64h-576v64h256v576h64v-576h256z"></path>
|
||||
<path d="M0 416h128v288h64v-288h128v-64h-320z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 277 B |
14
src/lib/icons/time.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path
|
||||
d="M651 271.4c0.8-0.7 1.5-1.4 2.3-2.2 25-25 25-65.6 0-90.5-12.1-12-28.2-18.7-45.3-18.7s-33.2 6.7-45.3 18.7c-0.7 0.7-1.5 1.5-2.2 2.3-43.2-28.7-92.5-46.3-144.6-51.4v-33.6h16c26.5 0 48-21.5 48-48s-21.4-48-47.9-48h-96c-26.5 0-48 21.5-48 48s21.5 48 48 48h16v33.6c-73.4 7.2-141.4 39.3-194.3 92.2-60.4 60.4-93.7 140.7-93.7 226.2s33.3 165.8 93.7 226.3c60.5 60.4 140.8 93.7 226.3 93.7s165.8-33.3 226.3-93.7c60.4-60.5 93.7-140.8 93.7-226.3 0-63.8-18.5-124.7-53-176.6zM320 48c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16h-96c-8.8 0-16-7.2-16-16zM608 192c8.5 0 16.6 3.3 22.6 9.4 12.1 12.1 12.5 31.5 1.1 44.1-6.7-8.2-13.9-16.1-21.5-23.7s-15.5-14.8-23.7-21.5c6-5.4 13.5-8.3 21.5-8.3zM384 704c-141.2 0-256-114.8-256-256s114.8-256 256-256c141.2 0 256 114.8 256 256s-114.8 256-256 256z"
|
||||
></path>
|
||||
<path
|
||||
d="M447 436.3l-31.5-185.8v-0.3c-2.8-15.2-16.1-26.3-31.5-26.3s-28.6 10.9-31.4 26l-31.6 186.4c-0.7 4-1 7.9-1 11.7 0 35.3 28.7 64 64 64s64-28.7 64-64c0-3.7-0.4-7.6-1-11.7v0zM384 480c-17.6 0-32-14.4-32-32 0-2 0.2-4.1 0.6-6.3l31.3-185.7c0 0 0 0 0 0.1l31.4 185.2h-0.1c0.5 2.4 0.7 4.7 0.7 6.7 0.1 17.6-14.3 32-31.9 32z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
8
src/lib/icons/tuning-fork.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 768 768">
|
||||
<path d="M721.4 97.4l-368 368c-21.9 21.9-42.8 16.6-55.1 4.3s-17.6-33.2 4.3-55.1l368-368-45.2-45.2-368 368c-28.3 28.3-33.9 57.7-33.6 77.4 0.2 14.7 3.8 29 10.3 42l-153.5 153.4c-5.3-1.4-10.9-2.2-16.6-2.2-17.1 0-33.2 6.7-45.3 18.7-25 25-25 65.6 0 90.5 12.1 12.1 28.2 18.8 45.3 18.8s33.2-6.7 45.3-18.7c16.7-16.7 22.2-40.4 16.6-61.8l153.4-153.5c13 6.5 27.3 10.1 42 10.3 0.5 0 1 0 1.5 0 19.6 0 48.3-6 75.9-33.6l368-368-45.3-45.3zM86.6 726.6c-6 6-14.1 9.4-22.6 9.4s-16.6-3.3-22.6-9.4c-12.5-12.5-12.5-32.8 0-45.3 0 0 0 0 0 0 6-6 14.1-9.4 22.6-9.4s16.6 3.3 22.6 9.4c12.5 12.5 12.5 32.8 0 45.3z"></path>
|
||||
<path d="M448 544v32c68.4 0 132.7-26.6 181-75s75-112.6 75-181h-32c0 123.5-100.5 224-224 224z"></path>
|
||||
<path d="M224 320c0-123.5 100.5-224 224-224v-32c-68.4 0-132.7 26.6-181 75s-75 112.6-75 181h32z"></path>
|
||||
<path d="M736 320c0 158.8-129.2 288-288 288v32c85.5 0 165.8-33.3 226.3-93.7 60.4-60.5 93.7-140.8 93.7-226.3h-32z"></path>
|
||||
<path d="M448 32v-32c-85.5 0-165.8 33.3-226.3 93.7-60.4 60.5-93.7 140.8-93.7 226.3h32c0-158.8 129.2-288 288-288z"></path>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
src/lib/icons/user.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<g id="icomoon-ignore"> </g>
|
||||
<path
|
||||
d="M384 448c105.9 0 192-86.1 192-192s-86.1-192-192-192-192 86.1-192 192 86.1 192 192 192zM384 128c70.6 0 128 57.4 128 128s-57.4 128-128 128c-70.6 0-128-57.4-128-128s57.4-128 128-128z"
|
||||
></path>
|
||||
<path
|
||||
d="M630.7 546.2c-44.2-44.6-124.9-66.2-246.7-66.2s-202.5 21.6-246.7 66.2c-41.3 41.6-41.3 92.3-41.3 125.8v16c0 8.8 7.2 16 16 16h544c8.8 0 16-7.2 16-16v-16c0-33.5 0-84.2-41.3-125.8zM160 672c0-28.4 0-57.9 22.7-80.7 13.5-13.6 34.3-24.4 62-32.2 35.4-10 82.3-15.1 139.3-15.1s103.9 5.1 139.3 15.1c27.7 7.8 48.5 18.6 62 32.2 22.7 22.8 22.7 52.3 22.7 80.7h-448z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 755 B |
22
src/lib/icons/weight.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 768 768"
|
||||
>
|
||||
<path d="M160 576h160v32h-160v-32z"></path>
|
||||
<path d="M160 448h160v32h-160v-32z"></path>
|
||||
<path d="M160 384h160v32h-160v-32z"></path>
|
||||
<path d="M448 576h160v32h-160v-32z"></path>
|
||||
<path d="M448 448h160v32h-160v-32z"></path>
|
||||
<path d="M448 384h160v32h-160v-32z"></path>
|
||||
<path d="M160 512h160v32h-160v-32z"></path>
|
||||
<path d="M448 512h160v32h-160v-32z"></path>
|
||||
<path
|
||||
d="M292 314.5c3 3.5 7.4 5.5 12 5.5h160c4.6 0 9-2 12-5.5l64-73c2.8-3.3 4.3-7.5 3.9-11.8-0.3-4.3-2.4-8.3-5.7-11-48.4-40-97.4-58.7-154.2-58.7s-105.8 18.7-154.2 58.7c-3.3 2.8-5.4 6.7-5.7 11s1.1 8.6 3.9 11.8l64 73zM384 192c44.6 0 82.4 12.8 120.8 41.2l-48 54.8h-46l6.9-62.1-31.8-3.5-7.3 65.7h-67.3l-48-54.8c38.3-28.5 76.1-41.3 120.7-41.3z"
|
||||
></path>
|
||||
<path
|
||||
d="M608 32h-448c-70.6 0-128 57.4-128 128v448c0 70.6 57.4 128 128 128h448c70.6 0 128-57.4 128-128v-448c0-70.6-57.4-128-128-128zM672 608c0 35.3-28.7 64-64 64h-448c-35.3 0-64-28.7-64-64v-448c0-35.3 28.7-64 64-64h448c35.3 0 64 28.7 64 64v448z"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
src/lib/icons/widescreen.svelte
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="100%" height="100%">
|
||||
<path d="M704 64h-640c-35.3 0-64 28.7-64 64v352c0 35.3 28.7 64 64 64h288v64c0 15.1-12.4 30.6-34 42.6-24.9 13.8-58.3 21.4-94 21.4v32h320v-32c-35.8 0-69.2-7.6-94-21.4-21.6-12-34-27.5-34-42.6v-64h288c35.3 0 64-28.7 64-64v-352c0-35.3-28.7-64-64-64zM423.8 672h-79.6c24.9-16.9 39.8-39.2 39.8-64 0 24.8 14.9 47.1 39.8 64zM64 128h640v288h-640v-288c-0.1 0 0 0 0 0zM64 480v-32h640v32h-640z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
7
src/lib/icons/window.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<!-- generated by icomoon.io - licensed Lindua icon -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100%" height="100%">
|
||||
<path d="M22 1h-20c-1.103 0-2 0.897-2 2v18c0 1.103 0.897 2 2 2h20c1.103 0 2-0.897 2-2v-18c0-1.103-0.897-2-2-2zM22 3v4h-20v-4h20zM22 8v11h-20v-11h20zM22 21h-20v-1h20.003l-0.003 1c0.003 0 0 0 0 0z"></path>
|
||||
<path d="M5 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
|
||||
<path d="M8 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
|
||||
<path d="M11 5c0 0.552-0.448 1-1 1s-1-0.448-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
8
src/lib/interfaces/DNS.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Record {
|
||||
name: string;
|
||||
ttl: number;
|
||||
class: string;
|
||||
type: string;
|
||||
data: string;
|
||||
available?: boolean;
|
||||
}
|
||||
11
src/lib/interfaces/health.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum HEALTH_STATUS {
|
||||
LIVE = 'live',
|
||||
UNKNOWN = 'unknown',
|
||||
DOWN = 'down'
|
||||
}
|
||||
|
||||
export interface HttpEndpoint {
|
||||
domain: string;
|
||||
code: number;
|
||||
status: HEALTH_STATUS.DOWN;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
export interface Filament {
|
||||
Hex: string;
|
||||
Color: string;
|
||||
Material: string;
|
||||
Weight: string;
|
||||
Count: number;
|
||||
Link: string;
|
||||
hex: string;
|
||||
color: string;
|
||||
material: string;
|
||||
weight: number;
|
||||
count: number;
|
||||
link: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,45 @@ interface NodeStatus {
|
||||
bootInfo: BootInfo;
|
||||
}
|
||||
|
||||
export interface VM {
|
||||
cpu: number;
|
||||
cpus: number;
|
||||
disk: number;
|
||||
diskread: number;
|
||||
diskwrite: number;
|
||||
maxdisk: number;
|
||||
maxmem: number;
|
||||
mem: number;
|
||||
name: string;
|
||||
netin: number;
|
||||
netout: number;
|
||||
pid: number;
|
||||
status: string;
|
||||
uptime: number;
|
||||
vmid: number;
|
||||
}
|
||||
|
||||
export interface LXC {
|
||||
cpu: number;
|
||||
cpus: number;
|
||||
disk: number;
|
||||
diskread: number;
|
||||
diskwrite: number;
|
||||
maxdisk: number;
|
||||
maxmem: number;
|
||||
maxswap: number;
|
||||
mem: number;
|
||||
name: string;
|
||||
netin: number;
|
||||
netout: number;
|
||||
pid: number;
|
||||
status: string;
|
||||
swap: number;
|
||||
type: string;
|
||||
uptime: number;
|
||||
vmid: number;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
info: NodeStatus;
|
||||
online: number;
|
||||
@@ -76,4 +115,6 @@ export interface Node {
|
||||
type: string;
|
||||
ip: string;
|
||||
level: string;
|
||||
vms: Array<VM>;
|
||||
lxcs: Array<LXC>;
|
||||
}
|
||||
|
||||
9
src/lib/interfaces/site.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Site {
|
||||
name: string;
|
||||
link: string;
|
||||
image: string;
|
||||
color: string;
|
||||
background: string;
|
||||
created?: number;
|
||||
updated?: number;
|
||||
}
|
||||
22
src/lib/remote/filesystem.remote.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { prerender } from '$app/server';
|
||||
|
||||
export const allRoutes = prerender(() => {
|
||||
const modules = import.meta.glob('/src/routes/**/+page.svelte');
|
||||
const routes = Object.keys(modules).map((path) => {
|
||||
// Remove '/src/routes' prefix and '+page.svelte' suffix
|
||||
let route = path.replace('/src/routes', '').replace('/+page.svelte', '');
|
||||
// Handle the root route
|
||||
|
||||
route = route.toString().split('/')[1];
|
||||
return route;
|
||||
});
|
||||
|
||||
const allRoute = [...new Set(routes)].map((r: string) => {
|
||||
return {
|
||||
name: r?.length > 1 ? r[0].toUpperCase() + r.slice(1, r.length) : r,
|
||||
path: '/' + r
|
||||
};
|
||||
});
|
||||
|
||||
return [{ name: 'Home', path: '/' }, ...allRoute].filter((r) => r.name);
|
||||
});
|
||||