Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aceberg/AnyAppStart
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.1.1
Choose a base ref
...
head repository: aceberg/AnyAppStart
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 16 commits
  • 45 files changed
  • 1 contributor

Commits on Jan 8, 2025

  1. MobX

    aceberg committed Jan 8, 2025
    Copy the full SHA
    add61ef View commit details
  2. Goroutines, Icons

    aceberg committed Jan 8, 2025
    Copy the full SHA
    82a15df View commit details

Commits on Jan 9, 2025

  1. Filter, Sort upd

    aceberg committed Jan 9, 2025
    Copy the full SHA
    3ea3e33 View commit details
  2. Dockerfile

    aceberg committed Jan 9, 2025
    Copy the full SHA
    3e0edd3 View commit details
  3. fix

    aceberg committed Jan 9, 2025
    Copy the full SHA
    5bfcbef View commit details
  4. Release 0.1.2

    aceberg committed Jan 9, 2025
    Copy the full SHA
    f64bfeb View commit details
  5. README

    aceberg committed Jan 9, 2025
    Copy the full SHA
    5a677b2 View commit details

Commits on Jan 11, 2025

  1. README and docs

    aceberg committed Jan 11, 2025
    Copy the full SHA
    2a84bbe View commit details
  2. README upd

    aceberg committed Jan 11, 2025
    Copy the full SHA
    d98d284 View commit details
  3. README upd

    aceberg committed Jan 11, 2025
    Copy the full SHA
    81151fa View commit details
  4. README upd

    aceberg committed Jan 11, 2025
    Copy the full SHA
    7bc44ba View commit details

Commits on Jan 17, 2025

  1. typo fix

    aceberg committed Jan 17, 2025
    Copy the full SHA
    f93e52d View commit details

Commits on Jan 18, 2025

  1. Try SSH from Docker

    aceberg committed Jan 18, 2025
    Copy the full SHA
    71a59d5 View commit details

Commits on Feb 26, 2025

  1. CPU, Memory stat

    aceberg committed Feb 26, 2025
    Copy the full SHA
    0a28202 View commit details
  2. Try SSH from Docker

    aceberg committed Feb 26, 2025
    Copy the full SHA
    9b54aa6 View commit details
  3. Fetch Items, Toast

    aceberg committed Feb 26, 2025
    Copy the full SHA
    3acbbbc View commit details
Showing with 1,192 additions and 365 deletions.
  1. +26 −0 .github/workflows/dev-docker-io.yml
  2. +57 −0 .github/workflows/main-docker.yml
  3. +28 −0 .github/workflows/readme-dockerhub.yml
  4. +1 −1 .version
  5. +7 −0 CHANGELOG.md
  6. +15 −0 Dockerfile
  7. +131 −5 README.md
  8. BIN assets/Screenshot_00.png
  9. BIN assets/Screenshot_02.png
  10. BIN assets/Screenshot_03.png
  11. BIN assets/Screenshot_04.png
  12. BIN assets/Screenshot_05.png
  13. +7 −0 backend/internal/models/models.go
  14. +7 −2 backend/internal/service/exec.go
  15. +11 −6 backend/internal/web/api.go
  16. +12 −16 backend/internal/web/functions.go
  17. +1 −1 backend/internal/web/public/assets/index.css
  18. +206 −20 backend/internal/web/public/assets/index.js
  19. +61 −0 docs/API.md
  20. +40 −0 docs/BUILD.md
  21. +31 −0 example/types.yaml
  22. +72 −5 frontend/package-lock.json
  23. +5 −2 frontend/package.json
  24. +16 −4 frontend/src/App.css
  25. +12 −2 frontend/src/App.tsx
  26. +38 −50 frontend/src/components/Body.tsx
  27. +0 −25 frontend/src/components/BodyAddItem.tsx
  28. +25 −0 frontend/src/components/BodyGroupFilter.tsx
  29. +0 −72 frontend/src/components/BodyHeader.tsx
  30. +17 −31 frontend/src/components/BodyTabs.tsx
  31. +2 −2 frontend/src/components/ConfigAbout.tsx
  32. +29 −0 frontend/src/components/ConfigAddItem.tsx
  33. +3 −1 frontend/src/components/ConfigDropdown.tsx
  34. +8 −5 frontend/src/components/ConfigSettings.tsx
  35. +30 −8 frontend/src/components/EditItem.tsx
  36. +40 −31 frontend/src/components/Header.tsx
  37. +32 −22 frontend/src/components/ItemShow.tsx
  38. +4 −1 frontend/src/components/TypeAdd.tsx
  39. +19 −7 frontend/src/components/TypeEdit.tsx
  40. +7 −3 frontend/src/components/TypesList.tsx
  41. +2 −41 frontend/src/functions/api.tsx
  42. +32 −0 frontend/src/functions/exports.tsx
  43. +2 −2 frontend/src/functions/sortitems.tsx
  44. +103 −0 frontend/src/functions/store.tsx
  45. +53 −0 frontend/src/functions/updstate.ts
26 changes: 26 additions & 0 deletions .github/workflows/dev-docker-io.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Dev-to-docker

on:
workflow_dispatch:

env:
IMAGE_NAME: anyappstart
TAGS: dev


jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Build and Push Docker Image to docker.io
uses: mr-smithers-excellent/docker-build-push@v6
with:
image: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}
tags: ${{ env.TAGS }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
57 changes: 57 additions & 0 deletions .github/workflows/main-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Main-Docker

on:
workflow_dispatch:
# push:
# branches: [ "main" ]
# paths:
# - 'Dockerfile'
# - 'src/**'

env:
IMAGE_NAME: anyappstart

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Get version tag from env file
uses: c-py/action-dotenv-to-setenv@v5
with:
env-file: .version

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/i386,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
28 changes: 28 additions & 0 deletions .github/workflows/readme-dockerhub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Readme-DockerHub

on:
workflow_dispatch:
push:
branches: [ "main" ]
paths:
- 'README.md'

env:
IMAGE_NAME: anyappstart

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Sync README.md to DockerHub
uses: ms-jpq/sync-dockerhub-readme@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}
readme: "./README.md"

2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION=0.1.1
VERSION=0.1.2
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,13 @@
# Change Log
All notable changes to this project will be documented in this file.

## [0.1.2] - 2025-01-09
### Added
- MobX store
- Goroutine for getting states
- Icons for items
- Filter and Sort update

## [0.1.1] - 2025-01-08
### Added
- Variable `$ITEMNAME`
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM golang:alpine AS builder

RUN apk add build-base
COPY backend /src
RUN cd /src/cmd/AnyAppStart/ && CGO_ENABLED=0 go build -o /AnyAppStart .


FROM alpine:3

RUN apk add --no-cache docker tzdata openssh

WORKDIR /app
COPY --from=builder /AnyAppStart /app/

ENTRYPOINT ["./AnyAppStart"]
136 changes: 131 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,145 @@
<h1><a href="https://github.com/aceberg/AnyAppStart">
<img src="https://raw.githubusercontent.com/aceberg/AnyAppStart/main/assets/logo.png" width="20" />
</a>AnyAppStart</h1>
<br/>

`Start/Stop/Restart/View Logs` for Docker, Systemd, SSH, VMs and anything else
[![Main-Docker](https://github.com/aceberg/AnyAppStart/actions/workflows/main-docker.yml/badge.svg)](https://github.com/aceberg/AnyAppStart/actions/workflows/main-docker.yml)
[![Binary-release](https://github.com/aceberg/AnyAppStart/actions/workflows/binary-release.yml/badge.svg)](https://github.com/aceberg/AnyAppStart/actions/workflows/binary-release.yml)
![GitHub License](https://img.shields.io/github/license/aceberg/AnyAppStart)

![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_01.png)
Control panel to `Start`/`Stop`/`Restart`/`View Logs` for Docker, Systemd, VMs or anything else (with user scripts)

- Backend: `Go`, Frontend: `React`, `TypeScript`, `MobX`
- Small lightweight app, easy to set up and configure
- User can add any types (like LXC or WakeOnLAN)
- Control remote machines via SSH
- Config in `yaml` files, no DB
- Simple API

![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_05.png)

## More screenshots
<details>
<summary>Expand</summary>

![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_04.png)
![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_03.png)
![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_02.png)
![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_03.png)
![Screenshot](https://raw.githubusercontent.com/aceberg/AnyAppStart/refs/heads/main/assets/Screenshot_04.png)

</details>

## Installation
<details>
<summary>Expand</summary>

> :warning: **Warning**
> 1. There is Docker image available, but inside the container only Docker Type will work, which kinda defeats the purpose of this app. So installing binary is recommended.
> 2. There is no built-in auth in this app, so make sure to restrict access to it with firewall and/or SSO (like Authelia) or simple [ForAuth](https://github.com/aceberg/ForAuth)
### Binary
All binary packages can be found in [latest](https://github.com/aceberg/AnyAppStart/releases/latest) release. There are `.deb`, `.rpm`, `.apk` (Alpine Linux) and `.tar.gz` files.

Supported architectures: `amd64`, `i386`, `arm_v5`, `arm_v6`, `arm_v7`, `arm64`.

For `amd64` there is a `deb` repo [available](https://github.com/aceberg/ppa)

### Docker
For demo purposes, mostly.
```sh
docker run --name AnyAppStart \
-e "TZ=$YOURTIMEZONE" \
-v ~/.dockerdata/AnyAppStart:/data/AnyAppStart \ # yaml files here
-v /var/run/docker.sock:/var/run/docker.sock \ # mount docker
-p 8855:8855 \
aceberg/anyappstart
```

</details>

## Usage
<details>
<summary>Expand</summary>

To run AnyAppStart as user, enable and start it:
```sh
sudo systemctl enable --now AnyAppStart@$USER.service
```
After, you need to fill `types.yaml` file, either manually by clicking `Add Type` in GUI Types menu, or by copying this [types.yaml](https://github.com/aceberg/AnyAppStart/blob/main/example/types.yaml) example to `~/.config/AnyAppStart/` (or `/etc/AnyAppStart/` for root)
```yaml
# $ITEMNAME is a variable that will be parsed into actual Items names
Docker:
Logs: docker logs $ITEMNAME
Restart: docker restart $ITEMNAME
Start: docker start $ITEMNAME
State: docker ps --filter status=running | grep $ITEMNAME
Stop: docker stop $ITEMNAME
Systemd:
Logs: sudo systemctl status $ITEMNAME
Restart: sudo systemctl restart $ITEMNAME
Start: sudo systemctl start $ITEMNAME
State: sudo systemctl | grep running | grep $ITEMNAME
Stop: sudo systemctl stop $ITEMNAME
VM:
Logs: sudo journalctl -u libvirtd.service
Restart: sudo virsh reboot $ITEMNAME
Start: sudo virsh start $ITEMNAME
State: sudo virsh list --state-running | grep $ITEMNAME
Stop: sudo virsh shutdown $ITEMNAME
ssh-Docker:
Logs: ssh remote-host-ip -f docker logs $ITEMNAME
Restart: ssh remote-host-ip -f docker restart $ITEMNAME
Start: ssh remote-host-ip -f docker start $ITEMNAME
State: ssh remote-host-ip -f docker ps --filter status=running | grep $ITEMNAME
Stop: ssh remote-host-ip -f docker stop $ITEMNAME
ssh-Systemd:
Logs: ssh remote-host-ip -f sudo systemctl status $ITEMNAME
Restart: ssh remote-host-ip -f sudo systemctl restart $ITEMNAME
Start: ssh remote-host-ip -f sudo systemctl start $ITEMNAME
State: ssh remote-host-ip -f sudo systemctl | grep running | grep $ITEMNAME
Stop: ssh remote-host-ip -f systemctl stop $ITEMNAME
```
</details>
## Config
<details>
<summary>Expand</summary>
| Variable | Description | Default |
| -------- | ----------- | ------- |
| TZ | Set your timezone for correct time | |
| HOST | Listen address | 0.0.0.0 |
| PORT | Port for web GUI | 8855 |
| THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) | minty |
| COLOR | Background color: light or dark | dark |
| NODEPATH | Path to local [node modules](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) | |
</details>
## Options
<details>
<summary>Expand</summary>
| Key | Description | Default | Systemd (root) | Systemd (user) |
| -------- | ----------- | ------- | ------- | ------- |
| -d | Path to config dir | /data/AnyAppStart | /etc/AnyAppStart | ~/.config/AnyAppStart |
| -n | Path to local [node modules](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) | | | |
</details>
## Build (for devs) and API
<details>
<summary>Expand</summary>
- API: [docs/API.md](docs/API.md)
- Build: [docs/BUILD.md](docs/BUILD.md)
</details>
## Thanks
<details>
<summary>Expand</summary>
- All go packages listed in [dependencies](https://github.com/aceberg/DiaryMD/network/dependencies)
- Favicon and logo: [Flaticon](https://www.flaticon.com)
- [Bootstrap](https://getbootstrap.com/)
</details>
Binary file added assets/Screenshot_00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Screenshot_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Screenshot_03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Screenshot_04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Screenshot_05.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions backend/internal/models/models.go
Original file line number Diff line number Diff line change
@@ -15,12 +15,16 @@ type Conf struct {

// Item - one service or container
type Item struct {
ID int `yaml:"ID"`
Group string `yaml:"group"`
Name string `yaml:"name"`
Type string `yaml:"type"`
Link string `yaml:"link,omitempty"`
Icon string `yaml:"icon,omitempty"`
Exec string `yaml:"-"`
State string `yaml:"-"`
Mem string `yaml:"-"`
CPU string `yaml:"-"`
}

// TypeStruct - one type struct
@@ -31,4 +35,7 @@ type TypeStruct struct {
Stop string
Logs string
State string
Mem string
CPU string
SSH string
}
9 changes: 7 additions & 2 deletions backend/internal/service/exec.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package service

import (
"log"
// "log"
"os/exec"
"strings"

@@ -20,7 +20,12 @@ func Exec(item models.Item, typesMap map[string]map[string]string) (bool, string

str = strings.Replace(str, "$ITEMNAME", item.Name, -1)

log.Println("String to exec:", str)
ssh := t["SSH"]
if ssh != "" {
str = ssh + " " + str
}

// log.Println("EXEC:", str)
cmd := exec.Command("sh", "-c", str)

out, err := cmd.CombinedOutput()
17 changes: 11 additions & 6 deletions backend/internal/web/api.go
Original file line number Diff line number Diff line change
@@ -49,7 +49,12 @@ func apiExec(c *gin.Context) {
func apiGetItems(c *gin.Context) {

items := yaml.Read(appConfig.ItemPath)
items = getAllStates(items)

sort.Slice(items, func(i, j int) bool {
return items[i].Name < items[j].Name
})

items = setItemIDs(items)

c.IndentedJSON(http.StatusOK, items)
}
@@ -71,7 +76,7 @@ func apiSaveItem(c *gin.Context) {
items = append(items, newItem)
} else {
for _, item := range yaml.Read(appConfig.ItemPath) {
if item == oldItem {
if item.Name == oldItem.Name && item.Type == oldItem.Type {
if newItem.Name != "" {
items = append(items, newItem)
}
@@ -93,7 +98,7 @@ func apiSaveConf(c *gin.Context) {
err := json.Unmarshal([]byte(str), &config)
check.IfError(err)

log.Println("CONF", config)
// log.Println("CONF", config)
appConfig.Host = config.Host
appConfig.Port = config.Port
appConfig.Theme = config.Theme
@@ -130,8 +135,9 @@ func apiSaveType(c *gin.Context) {

types := yaml.ReadTypes(appConfig.TypePath)

log.Println("OLD:", oldType)
log.Println("NEW:", newType)
if types == nil {
types = make(map[string]map[string]string)
}

if oldType.Name == "" && newType.Name != "" { // If new type
types[newType.Name] = toOneType(newType)
@@ -148,7 +154,6 @@ func apiSaveType(c *gin.Context) {
}
}

log.Println("TYPES:", types)
yaml.WriteTypes(appConfig.TypePath, types)

c.IndentedJSON(http.StatusOK, true)
28 changes: 12 additions & 16 deletions backend/internal/web/functions.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package web

import (
// "log"

"github.com/aceberg/AnyAppStart/internal/models"
"github.com/aceberg/AnyAppStart/internal/service"
"github.com/aceberg/AnyAppStart/internal/yaml"
)

func typesToStruct(types map[string]map[string]string) (typeStructArray []models.TypeStruct) {
@@ -18,6 +18,9 @@ func typesToStruct(types map[string]map[string]string) (typeStructArray []models
oneStruct.Stop = value["Stop"]
oneStruct.Logs = value["Logs"]
oneStruct.State = value["State"]
oneStruct.Mem = value["Mem"]
oneStruct.CPU = value["CPU"]
oneStruct.SSH = value["SSH"]

typeStructArray = append(typeStructArray, oneStruct)
}
@@ -34,26 +37,19 @@ func toOneType(tStruct models.TypeStruct) (tmpMap map[string]string) {
tmpMap["Restart"] = tStruct.Restart
tmpMap["Logs"] = tStruct.Logs
tmpMap["State"] = tStruct.State
tmpMap["Mem"] = tStruct.Mem
tmpMap["CPU"] = tStruct.CPU
tmpMap["SSH"] = tStruct.SSH

return tmpMap
}

func getAllStates(items []models.Item) (newItems []models.Item) {
var ok bool

types := yaml.ReadTypes(appConfig.TypePath)
func setItemIDs(items []models.Item) []models.Item {
var newItems []models.Item

for _, item := range items {

item.Exec = "State"
ok, _ = service.Exec(item, types)
if ok {
item.State = "on"
} else {
item.State = "off"
}
for i, item := range items {
item.ID = i
newItems = append(newItems, item)
}

return newItems
}
2 changes: 1 addition & 1 deletion backend/internal/web/public/assets/index.css
226 changes: 206 additions & 20 deletions backend/internal/web/public/assets/index.js

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## API
```http
GET /api/conf
```
Get app config


```http
GET /api/exec?name=NAME&type=TYPE&exec=EXEC
```
Execute a command, where
- `NAME` - Item name
- `TYPE` - Item Type
- `EXEC` - one of the commands: Start, Stop, Restart, Logs, State

Returns:
- `Ok` - `true` or, if error occurred `false`
- `Out` - command output

<details>
<summary>Example</summary>

```sh
curl "http://0.0.0.0:8855/api/exec?name=wyl&type=Docker&exec=Start"
```
```json
{
"Ok": true,
"Out": "wyl\n"
}
```

</details>
<br>

```http
GET /api/items
```
Get all Items with their current states

```http
GET /api/types
```
Get all Types

```http
POST /api/conf
```
Save config variable `conf`. Example: `data.set('conf', JSON.stringify(conf))`
```http
POST /api/item
```
Edit Item. Variables:
- `old` - old Item
- `new` - new Item (empty Name to delete Item)
```http
POST /api/type
```
Edit Type. Variables:
- `old` - old Type
- `new` - new Type (empty Name to delete Type)
40 changes: 40 additions & 0 deletions docs/BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# For developers

This app has separate frontend (`React`) and backend (`Go`). You need to run both of them to see modifications while editing the code.

## Dev

1. Backend
```sh
cd backend/ #
make check # check golang code
make run # run backend
```
The backend needs to be restarted to apply any modifications to it!

2. Frontend
```sh
cd frontend/
npm i # install node modules
npm run dev # run frontend
```
Make sure the `api` port in `frontend/src/functions/api.tsx` is the same the backend uses.

## Build
To build the app in a single file, there is a `frontend/Makefile`:
```sh
cd frontend/
make all
```
Edit the `api` port in the Makefile if needed.

Then I just push the code to Github and let Actions build Docker and Binary files for me=)

### Build locally
To build binary locally, after the steps above, run:

```sh
cd backend/
make go-build
```
Binary file will be in `backend/tmp/AnyAppStart`
31 changes: 31 additions & 0 deletions example/types.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# $ITEMNAME is a variable that will be parsed into actual Items names
Docker:
Logs: docker logs $ITEMNAME
Restart: docker restart $ITEMNAME
Start: docker start $ITEMNAME
State: docker ps --filter status=running | grep $ITEMNAME
Stop: docker stop $ITEMNAME
Systemd:
Logs: sudo systemctl status $ITEMNAME
Restart: sudo systemctl restart $ITEMNAME
Start: sudo systemctl start $ITEMNAME
State: sudo systemctl | grep running | grep $ITEMNAME
Stop: sudo systemctl stop $ITEMNAME
VM:
Logs: sudo journalctl -u libvirtd.service
Restart: sudo virsh reboot $ITEMNAME
Start: sudo virsh start $ITEMNAME
State: sudo virsh list --state-running | grep $ITEMNAME
Stop: sudo virsh shutdown $ITEMNAME
ssh-Docker:
Logs: ssh remote-host-ip -f docker logs $ITEMNAME
Restart: ssh remote-host-ip -f docker restart $ITEMNAME
Start: ssh remote-host-ip -f docker start $ITEMNAME
State: ssh remote-host-ip -f docker ps --filter status=running | grep $ITEMNAME
Stop: ssh remote-host-ip -f docker stop $ITEMNAME
ssh-Systemd:
Logs: ssh remote-host-ip -f sudo systemctl status $ITEMNAME
Restart: ssh remote-host-ip -f sudo systemctl restart $ITEMNAME
Start: ssh remote-host-ip -f sudo systemctl start $ITEMNAME
State: ssh remote-host-ip -f sudo systemctl | grep running | grep $ITEMNAME
Stop: ssh remote-host-ip -f systemctl stop $ITEMNAME
77 changes: 72 additions & 5 deletions frontend/package-lock.json
7 changes: 5 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "anyappstart",
"private": true,
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,8 +11,11 @@
},
"dependencies": {
"bootstrap": "^5.3.3",
"mobx": "^6.13.5",
"mobx-react-lite": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-hot-toast": "^2.5.2"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
20 changes: 16 additions & 4 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/* VARS */
:root {
--transparent-light: #ffffff15;
}

/* Transparant icon button */
.shade-hover {
padding: 9px;
border-radius: 15%;
}
.shade-hover:hover {
background-color: #00000015;
background-color: var(--transparent-light);
cursor: pointer;
}

@@ -15,15 +20,22 @@
border-color: var(--bs-primary);
border-style: solid;
align-content: center;
padding-inline: 10%;
padding-inline: 1.2em;
white-space: nowrap;
/* margin-inline: 0.1px; */
height: 2.5em;
}
.btn-tab:hover {
background-color: #00000015;
background-color: var(--transparent-light);
}

.btn-tab-main {
border-bottom-color: #00000000;
border-bottom-width: thick;
}

/* GR-FILTER */
.gr-filter {
background-color: #00000000;
cursor: pointer;
border-style: hidden;
}
14 changes: 12 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -2,14 +2,24 @@ import "bootstrap/js/dist/dropdown";
import './App.css'
import Body from './components/Body'
import Header from './components/Header'
import { useEffect } from "react";
import { fetchItems, updAllItems } from "./functions/updstate";

function App() {

useEffect(() => {
fetchItems();

setInterval(() => {
updAllItems();
}, 60000); // 60000 ms = 1 minute
}, []);

return (
<>
<div className="container-lg mb-2">
<Header></Header>
<Body></Body>
</>
</div>
)
}

88 changes: 38 additions & 50 deletions frontend/src/components/Body.tsx
Original file line number Diff line number Diff line change
@@ -1,99 +1,87 @@
import { useEffect, useState } from "react"
import { getItems, Item } from "../functions/api"
import ItemShow from "./ItemShow";
import { filterItems, getGroupsList, sortItems } from "../functions/sortitems";
import BodyTabs from "./BodyTabs";
import { Item } from "../functions/exports";
import { observer } from "mobx-react-lite";
import mobxStore from "../functions/store";
import BodyGroupFilter from "./BodyGroupFilter";

let sortField:keyof Item = "Exec";
let sortWay:boolean = true;
let filterField:keyof Item = "Exec";
let filterOption:string = "";
const Body: React.FC = observer(() => {

function Body() {
const stateOn = "bi bi-circle-fill text-success";
const stateOff = "bi bi-circle-fill text-danger";

const [items, setItems] = useState<Item[]>([]);
const [grList, setGrList] = useState<string[]>([]);
const [updBody, setUpdBody] = useState<boolean>(false);
const [sortTrigger, setSortTrigger] = useState<boolean>(false);

const handleSort = (sortby:keyof Item) => {
const handleSort = (sortBy:keyof Item) => {
setSortTrigger(!sortTrigger);
if (sortby === sortField) {
sortWay = !sortWay;
localStorage.setItem('sort_way', JSON.stringify(sortWay));
if (sortBy === mobxStore.sortField) {
mobxStore.setSortWay(!mobxStore.sortWay);
}
setItems(sortItems(items, sortby, sortWay, sortTrigger));
sortField = sortby;
localStorage.setItem('sort_field', sortField);
mobxStore.setItemFiltered(sortItems(mobxStore.itemFiltered, sortBy, mobxStore.sortWay, sortTrigger));
mobxStore.setSortField(sortBy);
}

const fetchData = async () => {
const str1 = localStorage.getItem('sort_field');
sortField = str1 as keyof Item;
const str = localStorage.getItem('sort_way');
sortWay = str === "true";
const str2 = localStorage.getItem('filter_field');
filterField = str2 as keyof Item;
const str3 = localStorage.getItem('filter_option');
filterOption = str3 as string;
const fetchData = () => {

let tmpItems:Item[] = await getItems();
let tmpItems:Item[] = mobxStore.itemList;
setGrList(getGroupsList(tmpItems));
if (filterOption !== "") {
tmpItems = filterItems(tmpItems, filterField, filterOption);
}

setItems(sortItems(tmpItems, sortField, sortWay, sortTrigger));
tmpItems = filterItems(tmpItems, "Type", mobxStore.getFilterType());
tmpItems = filterItems(tmpItems, "Group", mobxStore.getFilterGroup());

mobxStore.setItemFiltered(sortItems(tmpItems, mobxStore.getSortField(), mobxStore.getSortWay(), sortTrigger));
};

useEffect(() => {
fetchData();
setUpdBody(false);
}, [updBody]);

useEffect(() => { // Regular update
setInterval(() => {
fetchData();
}, 60000); // 60000 ms = 1 minute
}, []);
mobxStore.setUpdBody(false);
// console.log("BODY UPD");
}, [mobxStore.updBody]);

return (
<>
<div className="container-lg mt-2">
<div className="row mt-2">
<div className="col-md">
<div className="card border-primary">
<div className="card-header">
<BodyTabs grList={grList} setUpdBody={setUpdBody}></BodyTabs>
<BodyTabs></BodyTabs>
</div>
<div className="card-body table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th style={{ width: "1%" }}></th>
<th><i className="bi bi-circle-fill"></i><i onClick={() => handleSort("State")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th>Group<i onClick={() => handleSort("Group")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th>Name<i onClick={() => handleSort("Name")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th><i className="bi bi-circle"></i><i onClick={() => handleSort("State")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th>CPU</th>
<th>Mem</th>
<th>Type<i onClick={() => handleSort("Type")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th>Icon</th>
<th>Name<i onClick={() => handleSort("Name")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th><BodyGroupFilter grList={grList}></BodyGroupFilter><i onClick={() => handleSort("Group")} className="bi bi-sort-down-alt text-primary shade-hover"></i></th>
<th>&nbsp;&nbsp;Action</th>
<th>Logs</th>
<th>Edit</th>
<th style={{ width: "1%" }}>Link</th>
</tr>
</thead>
<tbody>
{items?.map((item, i) => (
{mobxStore.itemFiltered?.map((item, i) => (
<tr key={i}>
<td className="text-primary text-opacity-75">{i}.</td>
<ItemShow item={item} setUpdBody={setUpdBody}></ItemShow>
<td className="text-primary text-opacity-75">{i+1}.</td>
<td><i className={item.State == "on" ? stateOn : stateOff }></i></td>
<td>{item.CPU}</td>
<td>{item.Mem}</td>
<ItemShow item={item}></ItemShow>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</>
)
}
});

export default Body

25 changes: 0 additions & 25 deletions frontend/src/components/BodyAddItem.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions frontend/src/components/BodyGroupFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import mobxStore from "../functions/store";

function BodyGroupFilter(_props: any) {

const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = event.target.value;

mobxStore.setFilterGroup(selectedValue);
mobxStore.setUpdBody(true);
}

return (
<>
<select className="gr-filter" defaultValue="Groups" onChange={handleChange}>
<option value="Groups" disabled>Groups</option>
<option value="" title="All Groups">...</option>
{_props.grList?.map((key: string) => (
<option key={key} value={key}>{key}</option>
))}
</select>
</>
)
}

export default BodyGroupFilter
72 changes: 0 additions & 72 deletions frontend/src/components/BodyHeader.tsx

This file was deleted.

48 changes: 17 additions & 31 deletions frontend/src/components/BodyTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
import { useEffect } from "react";
import BodyAddItem from "./BodyAddItem";
import mobxStore from "../functions/store";
import { TypeStruct } from "../functions/exports";

let filterOption:string = "";

function BodyTabs(_props: any) {

const grList = _props.grList;
const typeList = mobxStore.typeList;

const handleFilter = (key: string) => {
filterOption = key;
localStorage.setItem('filter_field', "Group");
localStorage.setItem('filter_option', filterOption);
_props.setUpdBody(true);
setMainTab(filterOption);
};

const handleAny = () => {
filterOption = "";
localStorage.setItem('filter_field', "Exec");
localStorage.setItem('filter_option', filterOption);
_props.setUpdBody(true);
setMainTab(filterOption);
mobxStore.setFilterType(key);
mobxStore.setUpdBody(true);
setMainTab(key);
};

const setMainTab = (key: string) => {

document.getElementById("g1s52lVbKscc")?.classList.remove("btn-tab-main");
for (let i=0; i<grList.length; i++) {
document.getElementById(grList[i])?.classList.remove("btn-tab-main");
for (let i=0; i<typeList.length; i++) {
document.getElementById(typeList[i].Name)?.classList.remove("btn-tab-main");
}
key === "" ? key = "g1s52lVbKscc" : key = key;
document.getElementById(key)?.classList.add("btn-tab-main");
}

useEffect(() => {
filterOption = localStorage.getItem('filter_option') as string;
setMainTab(filterOption);
}, [filterOption]);
setMainTab(mobxStore.filterType);
}, [mobxStore.filterType]);

return (
<div className='d-flex justify-content-between'>
<div className="d-flex justify-content-left">
<button className="btn-tab rounded-top-3" onClick={handleAny} id="g1s52lVbKscc" title="All Groups">
<i className="bi bi-check2-all fs-5 px-2"></i>
</button>
{grList?.map((key: string) => (
<button key={key} onClick={() => handleFilter(key)} className="btn-tab rounded-top-3" id={key}>{key}</button>
))}
</div>
<BodyAddItem setUpdBody={_props.setUpdBody}></BodyAddItem>
<div className="d-flex justify-content-left flex-wrap">
<button className="btn-tab rounded-top-3" onClick={() => handleFilter("")} id="g1s52lVbKscc" title="All Types">
<i className="bi bi-check2-all fs-5"></i>
</button>
{typeList?.map((key: TypeStruct) => (
<button key={key.Name} onClick={() => handleFilter(key.Name)} className="btn-tab rounded-top-3" id={key.Name}>{key.Name}</button>
))}
</div>
)
}
4 changes: 2 additions & 2 deletions frontend/src/components/ConfigAbout.tsx
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@ function ConfigAbout() {
size=""
body={
<>
<p>About: <a href="https://github.com/aceberg/AnyAppStart">https://github.com/aceberg/AnyAppStart</a></p>
<p>Donate: <a href="https://github.com/aceberg#donate">https://github.com/aceberg#donate</a></p>
<p>About: <a href="https://github.com/aceberg/AnyAppStart" target="_blank">https://github.com/aceberg/AnyAppStart</a></p>
<p>Donate: <a href="https://github.com/aceberg#donate" target="_blank">https://github.com/aceberg#donate</a></p>
</>
}
onClose={handleCloseModal}
29 changes: 29 additions & 0 deletions frontend/src/components/ConfigAddItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Item } from "../functions/exports";
import EditItem from "./EditItem"


function ConfigAddItem(_props: any) {

const item:Item = {
ID: 0,
Group: "",
Name: "",
Type: "",
Link: "",
Icon: "",
State: "",
Exec: "",
CPU: "",
Mem: "",
};

return (
<>
<EditItem item={item}
btnContent={<a href="#" className="dropdown-item">Add Item</a>}>
</EditItem>
</>
)
}

export default ConfigAddItem
4 changes: 3 additions & 1 deletion frontend/src/components/ConfigDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ConfigAbout from "./ConfigAbout";
import ConfigAddItem from "./ConfigAddItem";
import ConfigSettings from "./ConfigSettings";

const ConfigDropdown = (_props: any) => {
@@ -8,7 +9,8 @@ const ConfigDropdown = (_props: any) => {
<i className="bi bi-gear shade-hover fs-3" data-bs-toggle="dropdown" title="Settings"></i>

<ul className="dropdown-menu">
<li><ConfigSettings headUpd={_props.headUpd}></ConfigSettings></li>
<li><ConfigAddItem></ConfigAddItem></li>
<li><ConfigSettings></ConfigSettings></li>
<li><hr className="dropdown-divider"></hr></li>
<li><ConfigAbout></ConfigAbout></li>
</ul>
13 changes: 8 additions & 5 deletions frontend/src/components/ConfigSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useState } from "react";
import BootstrapModal from "./Modal";
import { apiSaveConf, appConfig, Conf } from "../functions/api";
import { apiSaveConf } from "../functions/api";
import mobxStore from "../functions/store";
import { Conf } from "../functions/exports";

function ConfigSettings(_props: any) {

const appConfig = mobxStore.appConfig;
const [isModalOpen, setModalOpen] = useState<boolean>(false);
const [formData, setFormData] = useState<Conf>(appConfig);

@@ -25,8 +28,9 @@ function ConfigSettings(_props: any) {
const saveChanges = async () => {
if (JSON.stringify(formData) !== JSON.stringify(appConfig)) {
await apiSaveConf(formData);
console.log("SAVE:", formData);
_props.headUpd(true);
mobxStore.setAppConfig(formData);
console.log("CONFIG:", formData);
mobxStore.setUpdHead(true);
}
}

@@ -47,7 +51,6 @@ function ConfigSettings(_props: any) {
};

const handleColor = (event: React.ChangeEvent<HTMLSelectElement>) => {
console.log("SELECT", event.target.value);
setFormData((prev) => ({
...prev,
Color: event.target.value,
@@ -79,7 +82,7 @@ function ConfigSettings(_props: any) {
<option value="dark">dark</option>
</select>
<label htmlFor="tid" className="form-label text-primary">NodePath</label>
<input className="form-control mb-3" defaultValue={appConfig.NodePath} id="tid" name="NodePath" onChange={handleChange}></input>
<input className="form-control mb-3" defaultValue={appConfig.NodePath} id="tid" name="NodePath" onChange={handleChange} placeholder="Path to local node-bootstrap (optional)"></input>
<hr></hr>
<div className='d-flex justify-content-between'>
<span></span>
38 changes: 30 additions & 8 deletions frontend/src/components/EditItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { useState } from "react";
import BootstrapModal from "./Modal";
import { apiSaveItem, Item } from "../functions/api";
import { apiSaveItem } from "../functions/api";
import mobxStore from "../functions/store";
import { Item } from "../functions/exports";
import { fetchItems } from "../functions/updstate";

function EditItem(_props: any) {

const item:Item = {
ID: _props.item.ID,
Group: _props.item.Group,
Name: _props.item.Name,
Type: _props.item.Type,
Link: _props.item.Link,
Icon: _props.item.Icon,
Exec: "",
State: ""
State: "",
CPU: "",
Mem: "",
};

const [isModalOpen, setModalOpen] = useState<boolean>(false);
@@ -36,9 +43,10 @@ function EditItem(_props: any) {
const saveChanges = async () => {
if (JSON.stringify(formData) !== JSON.stringify(item)) {
await apiSaveItem(item, formData);
console.log("SAVE (before):", item);
console.log("SAVE (after):", formData);
_props.setUpdBody(true);

setTimeout(() => {
fetchItems();
}, 1000);
}
}

@@ -55,12 +63,19 @@ function EditItem(_props: any) {
setModalOpen(false);
}

const handleSelectType = (event: React.ChangeEvent<HTMLSelectElement>) => {
setFormData((prev) => ({
...prev,
Type: event.target.value,
}));
};

return (
<>
<span onClick={handleEdit}>{_props.btnContent}</span>
<BootstrapModal
isOpen={isModalOpen}
title="Edit Item"
title="Add/Edit Item"
size=""
body={
<form>
@@ -69,9 +84,16 @@ function EditItem(_props: any) {
<label htmlFor="nid" className="form-label text-primary">Name</label>
<input className="form-control mb-3" defaultValue={item.Name} id="nid" name="Name" onChange={handleChange} placeholder="Not empty string"></input>
<label htmlFor="tid" className="form-label text-primary">Type</label>
<input className="form-control mb-3" defaultValue={item.Type} id="tid" name="Type" onChange={handleChange}></input>
<select className="form-select mb-3" id="tid" onChange={handleSelectType} defaultValue={formData.Type}>
<option value="" disabled>Select type</option>
{mobxStore.typeList?.map((t, i) => (
<option key={i} value={t.Name}>{t.Name}</option>
))}
</select>
<label htmlFor="iid" className="form-label text-primary">Icon</label>
<input className="form-control mb-3" defaultValue={item.Icon} id="iid" name="Icon" onChange={handleChange} placeholder="Link to Icon (optional)"></input>
<label htmlFor="lid" className="form-label text-primary">Link</label>
<input className="form-control mb-3" defaultValue={item.Link} id="lid" name="Link" onChange={handleChange} placeholder="URL"></input>
<input className="form-control mb-3" defaultValue={item.Link} id="lid" name="Link" onChange={handleChange} placeholder="URL (optional)"></input>
<hr></hr>
<div className='d-flex justify-content-between'>
<button className="btn btn-danger" type="button" onClick={handleDel}>Delete</button>
71 changes: 40 additions & 31 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,64 @@
import { useEffect, useState } from "react";
import { getConfig, appConfig } from "../functions/api";
import { getConfig } from "../functions/api";
import ConfigDropdown from "./ConfigDropdown";
import TypesDropdown from "./TypesDropdown";
import mobxStore from "../functions/store";
import { observer } from "mobx-react-lite";
import { Conf } from "../functions/exports";

function Header() {
const Header: React.FC = observer(() => {

const [themePath, setThemePath] = useState('');
const [iconsPath, setIconsPath] = useState('');
const [updHead, setUpdHead] = useState<boolean>(false);

useEffect(() => {

const fetchData = async () => {

const setCurrentTheme = (appConfig:Conf) => {
const theme = appConfig.Theme?appConfig.Theme:"minty";
const color = appConfig.Color?appConfig.Color:"dark";

await getConfig();
if (appConfig.NodePath == '') {
setThemePath("https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/"+theme+"/bootstrap.min.css");
setIconsPath("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css");
} else {
setThemePath(appConfig.NodePath+"/node_modules/bootswatch/dist/"+theme+"/bootstrap.min.css");
setIconsPath(appConfig.NodePath+"/node_modules/bootstrap-icons/font/bootstrap-icons.css");
}

if (appConfig.NodePath == '') {
setThemePath("https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/"+appConfig.Theme+"/bootstrap.min.css");
setIconsPath("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css");
} else {
setThemePath(appConfig.NodePath+"/node_modules/bootswatch/dist/"+appConfig.Theme+"/bootstrap.min.css");
setIconsPath(appConfig.NodePath+"/node_modules/bootstrap-icons/font/bootstrap-icons.css");
}
document.documentElement.setAttribute("data-bs-theme", color);
color === "dark"
? document.documentElement.style.setProperty('--transparent-light', '#ffffff15')
: document.documentElement.style.setProperty('--transparent-light', '#00000015');
}

document.documentElement.setAttribute("data-bs-theme", appConfig.Color);
};

useEffect(() => {
const fetchData = async () => {
const appConfig = await getConfig();
mobxStore.setAppConfig(appConfig);
setCurrentTheme(appConfig);
}
fetchData();
setUpdHead(false);
}, [updHead]);

mobxStore.setUpdHead(false);
}, [mobxStore.updHead]);

const handleReload = () => {
window.location.reload();
};

return (
<>
<div className="row">
<link rel="stylesheet" href={iconsPath}></link> {/* icons */}
<link rel="stylesheet" href={themePath}></link> {/* theme */}
<div className="container-lg">
<div className='d-flex justify-content-between mt-2'>
<h3 className="shade-hover rounded-3" onClick={handleReload}>AnyAppStart</h3>
<div className='d-flex justify-content-between'>
<TypesDropdown></TypesDropdown>
<span className="p-3"></span>
<ConfigDropdown headUpd={setUpdHead}></ConfigDropdown>
</div>

<div className='d-flex justify-content-between mt-2'>
<h3 className="shade-hover rounded-3" onClick={handleReload}>AnyAppStart</h3>
<div className='d-flex justify-content-between'>
<TypesDropdown></TypesDropdown>
<span className="p-3"></span>
<ConfigDropdown></ConfigDropdown>
</div>
</div>
</>
</div>
)
}
});

export default Header
54 changes: 32 additions & 22 deletions frontend/src/components/ItemShow.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
import { apiExec } from "../functions/api";
import Logs from "./Logs";
import EditItem from "./EditItem";
import { updItemState } from "../functions/updstate";
import toast, { Toaster } from 'react-hot-toast';

function ItemShow(_props: any) {

const stateOn = "bi bi-circle-fill text-success";
const stateOff = "bi bi-circle-fill text-danger";

const handleExec = async (exec: string) => {
let item = _props.item;
item.Exec = exec;
_props.item.Exec = exec;

console.log("EXEC:", item);
await apiExec(item);
setTimeout(() => {
_props.setUpdBody(true);
}, 1000);
console.log("EXEC:", _props.item);
const res = await apiExec(_props.item);
updItemState(_props.item);
if (res.Ok) {
toast('"'+ exec +'" executed on "'+ _props.item.Name +'"', {
style: {
borderRadius: '10px',
background: '#000',
color: '#fff',
},
});
}
}

return (
<>
<td><i className={_props.item.State == "on" ? stateOn : stateOff }></i></td>
<td>{_props.item.Type}<Toaster position="top-center"/></td>
<td>
{_props.item.Icon
? <a href={_props.item.Link} target="_blank">
<img src={_props.item.Icon} width={30} height={30}></img>
</a>
: <></>
}
</td>
<td>
{_props.item.Link
? <a href={_props.item.Link} target="_blank">{_props.item.Name}</a>
: _props.item.Name
}
</td>
<td>{_props.item.Group}</td>
<td>{_props.item.Name}</td>
<td>{_props.item.Type}</td>
<td>
<i className="bi bi-play shade-hover me-1 fs-5" onClick={() => handleExec("Start")} title="Start"></i>
<i className="bi bi-arrow-clockwise shade-hover me-1 fs-5" onClick={() => handleExec("Restart")} title="Restart"></i>
@@ -33,20 +50,13 @@ function ItemShow(_props: any) {
<Logs item={_props.item}></Logs>
</td>
<td>
<EditItem item={_props.item} setUpdBody={_props.setUpdBody}
<EditItem item={_props.item}
btnContent={<i className="bi bi-three-dots-vertical shade-hover fs-5" title="Edit"></i>}>
</EditItem>
</td>
<td>
{_props.item.Link
? <a href={_props.item.Link} target="_blank">
<i className="bi bi-box-arrow-up-right shade-hover fs-5"></i>
</a>
: <></>
}
</td>
</>
)
}

export default ItemShow

5 changes: 4 additions & 1 deletion frontend/src/components/TypeAdd.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeStruct } from "../functions/api"
import { TypeStruct } from "../functions/exports"
import TypeEdit from "./TypeEdit"

function TypeAdd(_props:any) {
@@ -10,6 +10,9 @@ function TypeAdd(_props:any) {
Restart: "",
Logs: "",
State: "",
CPU: "",
Mem: "",
SSH: "",
}

return (
26 changes: 19 additions & 7 deletions frontend/src/components/TypeEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from "react";
import BootstrapModal from "./Modal";
import { apiSaveType, TypeStruct } from "../functions/api";
import { apiSaveType } from "../functions/api";
import { TypeStruct } from "../functions/exports";
import mobxStore from "../functions/store";

function TypeEdit(_props: any) {

@@ -10,7 +12,10 @@ function TypeEdit(_props: any) {
Stop: _props.typeItem.Stop,
Restart: _props.typeItem.Restart,
Logs: _props.typeItem.Logs,
State: _props.typeItem.State
State: _props.typeItem.State,
CPU: _props.typeItem.CPU,
Mem: _props.typeItem.Mem,
SSH: _props.typeItem.SSH
};

const [isModalOpen, setModalOpen] = useState<boolean>(false);
@@ -39,6 +44,7 @@ function TypeEdit(_props: any) {
console.log("SAVE2:", formData);
await apiSaveType(oldType, formData);
_props.setUpdTypes(true);
mobxStore.setUpdBody(true);
}
}

@@ -65,17 +71,23 @@ function TypeEdit(_props: any) {
<form>
<label htmlFor="nid" className="form-label text-primary">Name</label>
<input className="form-control mb-3" defaultValue={oldType.Name} id="nid" name="Name" onChange={handleChange} placeholder="Not empty string"></input>
<label htmlFor="sshid" className="form-label text-primary">SSH (optional)</label>
<input className="form-control mb-3" defaultValue={oldType.SSH} id="sshid" name="SSH" onChange={handleChange} placeholder="ssh -i /data/AnyAppStart/your_key -oStrictHostKeyChecking=no username@192.168.1.11 -f"></input>
<p>Use variable <code>$ITEMNAME</code> in the commands below</p>
<label htmlFor="gid" className="form-label text-primary">Start</label>
<input className="form-control mb-3" defaultValue={oldType.Start} id="gid" name="Start" onChange={handleChange} placeholder="Example: docker start $ITEMNAME"></input>
<input className="form-control mb-3" defaultValue={oldType.Start} id="gid" name="Start" onChange={handleChange} placeholder="docker start $ITEMNAME"></input>
<label htmlFor="oid" className="form-label text-primary">Stop</label>
<input className="form-control mb-3" defaultValue={oldType.Stop} id="oid" name="Stop" onChange={handleChange} placeholder="Example: docker stop $ITEMNAME"></input>
<input className="form-control mb-3" defaultValue={oldType.Stop} id="oid" name="Stop" onChange={handleChange} placeholder="docker stop $ITEMNAME"></input>
<label htmlFor="rid" className="form-label text-primary">Restart</label>
<input className="form-control mb-3" defaultValue={oldType.Restart} id="rid" name="Restart" onChange={handleChange} placeholder="Example: docker restart $ITEMNAME"></input>
<input className="form-control mb-3" defaultValue={oldType.Restart} id="rid" name="Restart" onChange={handleChange} placeholder="docker restart $ITEMNAME"></input>
<label htmlFor="lid" className="form-label text-primary">Logs</label>
<input className="form-control mb-3" defaultValue={oldType.Logs} id="lid" name="Logs" onChange={handleChange} placeholder="Example: docker logs $ITEMNAME"></input>
<input className="form-control mb-3" defaultValue={oldType.Logs} id="lid" name="Logs" onChange={handleChange} placeholder="docker logs $ITEMNAME"></input>
<label htmlFor="tid" className="form-label text-primary">State</label>
<input className="form-control mb-3" defaultValue={oldType.State} id="tid" name="State" onChange={handleChange} placeholder="Example: docker ps --filter status=running | grep $ITEMNAME"></input>
<input className="form-control mb-3" defaultValue={oldType.State} id="tid" name="State" onChange={handleChange} placeholder="docker ps --filter status=running | grep $ITEMNAME"></input>
<label htmlFor="cpuid" className="form-label text-primary">CPU</label>
<input className="form-control mb-3" defaultValue={oldType.CPU} id="cpuid" name="CPU" onChange={handleChange} placeholder="docker stats --no-stream --format '{{ .CPUPerc }}' $ITEMNAME"></input>
<label htmlFor="memid" className="form-label text-primary">Mem</label>
<input className="form-control mb-3" defaultValue={oldType.Mem} id="memid" name="Mem" onChange={handleChange} placeholder="docker stats --no-stream --format '{{ .MemUsage }}' $ITEMNAME | awk '{print $1}'"></input>
<hr></hr>
<div className='d-flex justify-content-between'>
<button className="btn btn-danger" type="button" onClick={handleDel}>Delete</button>
10 changes: 7 additions & 3 deletions frontend/src/components/TypesList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { useEffect, useState } from "react";
import { getTypes, TypeStruct } from "../functions/api";
import { getTypes } from "../functions/api";
import TypeEdit from "./TypeEdit";
import mobxStore from "../functions/store";
import { TypeStruct } from "../functions/exports";

function TypesList(_props:any) {

const [types, setTypes] = useState<TypeStruct[]>([]);

useEffect(() => {
const fetchData = async () => {

setTypes(await getTypes());

const t = await getTypes();
setTypes(t);
mobxStore.setTypeList(t);
};

fetchData();
43 changes: 2 additions & 41 deletions frontend/src/functions/api.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,10 @@
const api = 'http://0.0.0.0:8830';

export interface Item {
Group: string;
Name: string;
Type: string;
Link: string;
Exec: string;
State: string;
};

export interface Conf {
Host: string;
Port: string;
Theme: string;
Color: string;
NodePath: string;
};

export interface TypeStruct {
Name: string;
Start: string;
Stop: string;
Restart: string;
Logs: string;
State: string;
};

export let appConfig:Conf = {
Host: "",
Port: "",
Theme: "",
Color: "",
NodePath: ""
};
import { Conf, Item, TypeStruct } from "./exports";

export interface ToFilter {
Field: keyof Item;
Option: string;
};
const api = 'http://0.0.0.0:8830';

export const getConfig = async () => {
const url = api+'/api/conf';
const conf = await (await fetch(url)).json();

appConfig = conf;

return conf;
};
32 changes: 32 additions & 0 deletions frontend/src/functions/exports.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface Item {
ID: number;
Group: string;
Name: string;
Type: string;
Link: string;
Icon: string;
Exec: string;
State: string;
Mem: string;
CPU: string;
};

export interface Conf {
Host: string;
Port: string;
Theme: string;
Color: string;
NodePath: string;
};

export interface TypeStruct {
Name: string;
Start: string;
Stop: string;
Restart: string;
Logs: string;
State: string;
CPU: string;
Mem: string;
SSH: string;
};
4 changes: 2 additions & 2 deletions frontend/src/functions/sortitems.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Item } from "./api";
import { Item } from "./exports";

export const sortItems = (items:Item[], field:keyof Item, asc:boolean, trig:boolean) => {
trig = !trig; // trigger to run sort
@@ -18,7 +18,7 @@ function byField(a:Item, b:Item, fieldName:keyof Item, down:boolean){
};

export const filterItems = (items:Item[], field:keyof Item, option: string) => {
if ((items !== null) && (items.length > 0)) {
if ((items !== null) && (option !== "") && (items.length > 0)) {
items = items.filter((item) => item[field] == option);
}

103 changes: 103 additions & 0 deletions frontend/src/functions/store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { action, makeAutoObservable } from 'mobx';
import { Conf, Item, TypeStruct } from './exports';


class MobxStore {
constructor() {
makeAutoObservable(this, {
updItemList: action
});
}

itemList:Item[] = [];
setItemList(list:Item[]) {
this.itemList = list;
}
updItemList(newItem:Item) {
this.itemList.forEach((item, i) => {
if (item.ID == newItem.ID) {
action(() => {
this.itemList[i] = newItem
});
}
});
}

itemFiltered:Item[] = [];
setItemFiltered(list:Item[]) {
this.itemFiltered = list;
}

typeList:TypeStruct[] = [];
setTypeList(list:TypeStruct[]) {
this.typeList = list;
}

appConfig:Conf = {
Host: "",
Port: "",
Theme: "",
Color: "",
NodePath: ""
}
setAppConfig(conf:Conf) {
this.appConfig = conf;
}

updHead:boolean = false;
setUpdHead(b:boolean) {
this.updHead = b;
}

updBody:boolean = false;
setUpdBody(b:boolean) {
this.updBody = b;
}

sortField:keyof Item = "Exec";
setSortField(k:keyof Item) {
localStorage.setItem('sort_field', k);
this.sortField = k;
}
getSortField() {
const str = localStorage.getItem('sort_field');
this.sortField = str as keyof Item;
return this.sortField;
}

sortWay:boolean = true;
setSortWay(b:boolean) {
localStorage.setItem('sort_way', JSON.stringify(b));
this.sortWay = b;
}
getSortWay() {
const str = localStorage.getItem('sort_way');
this.sortWay = str === "true";
return this.sortWay;
}

filterGroup:string = "";
setFilterGroup(s:string) {
// localStorage.setItem('filter_group', s);
this.filterGroup = s;
}
getFilterGroup() {
// const str = localStorage.getItem('filter_group');
// this.filterGroup = str?str:"";
return this.filterGroup;
}

filterType:string = "";
setFilterType(s:string) {
localStorage.setItem('filter_type', s);
this.filterType = s;
}
getFilterType() {
const str = localStorage.getItem('filter_type');
this.filterType = str?str:"";
return this.filterType;
}
}

const mobxStore = new MobxStore();
export default mobxStore;
53 changes: 53 additions & 0 deletions frontend/src/functions/updstate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { apiExec, getItems } from "./api";
import { Item } from "./exports";
import mobxStore from "./store";

interface Res {
Ok: boolean;
Out: string;
}

export async function updItemState(item:Item) {

let res:Res;
item.Exec = "State";

res = await apiExec(item);

if (res.Ok) {
item.State = "on";

item.Exec = "CPU";
res = await apiExec(item);
item.CPU = res.Ok ? res.Out : "";

item.Exec = "Mem";
res = await apiExec(item);
item.Mem = res.Ok ? res.Out : "";
} else {
item.State = "off";
item.CPU = "";
item.Mem = "";
}

mobxStore.updItemList(item);
}

export function updAllItems() {

for (let item of mobxStore.itemList) {
// console.log("ITEM", item);
updItemState(item);
}
}

export const fetchItems = async () => {

const items = await getItems();
mobxStore.setItemList(items);
mobxStore.setUpdBody(true);

setTimeout(() => {
updAllItems();
}, 1000);
}