Compare commits
287 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e9de519ed | ||
![]() |
e148e4a19e | ||
![]() |
ea8aa94b68 | ||
![]() |
c67deabe01 | ||
![]() |
a9e0213b04 | ||
![]() |
3ac840262f | ||
![]() |
e08f3d1932 | ||
![]() |
9ef4c78712 | ||
![]() |
f8728e7114 | ||
![]() |
08c2e1ea97 | ||
![]() |
5d2f5dfe6c | ||
![]() |
691e1f1f9f | ||
![]() |
f4bbca3679 | ||
![]() |
6767118122 | ||
![]() |
39655e647d | ||
![]() |
24dd67a822 | ||
![]() |
b43c1b834b | ||
![]() |
7e224be0fc | ||
![]() |
1ba9bce628 | ||
![]() |
49e0c436d7 | ||
![]() |
5f24142c1a | ||
![]() |
1f1b9d22ec | ||
![]() |
ae169f15c7 | ||
![]() |
066e964d27 | ||
![]() |
91b2a67884 | ||
![]() |
afac2e8c58 | ||
![]() |
44188c5f5a | ||
![]() |
8bed0a4176 | ||
![]() |
49bdaca29f | ||
![]() |
f4208f9759 | ||
![]() |
7b636d57e7 | ||
![]() |
7cf9661b3a | ||
![]() |
6f6b4fb878 | ||
![]() |
4dd9939791 | ||
![]() |
437b2246fb | ||
![]() |
1e4e86849c | ||
![]() |
e4216c0481 | ||
![]() |
7168010566 | ||
![]() |
f15813c824 | ||
![]() |
a4232f9cd9 | ||
![]() |
bc14830c63 | ||
![]() |
0ee3aed47e | ||
![]() |
98693427ff | ||
![]() |
3651113817 | ||
![]() |
eadc54eafc | ||
![]() |
8968f2134d | ||
![]() |
08672c0195 | ||
![]() |
2064397351 | ||
![]() |
38f019a8d1 | ||
![]() |
a28ebdac5a | ||
![]() |
899c271f6c | ||
![]() |
8025b96bc2 | ||
![]() |
104a27e3b5 | ||
![]() |
ea4d188a6b | ||
![]() |
3f4a4283bf | ||
![]() |
61690c6df9 | ||
![]() |
f1821c19f1 | ||
![]() |
32329d66b5 | ||
![]() |
8e493b1925 | ||
![]() |
49c0cd8bfd | ||
![]() |
7180ca57b0 | ||
![]() |
8cf0d8fa0c | ||
![]() |
2520110268 | ||
![]() |
583e812353 | ||
![]() |
b716bf5176 | ||
![]() |
7768d6823c | ||
![]() |
3b3e0ccdd4 | ||
![]() |
3e660456ff | ||
![]() |
5b82a1b089 | ||
![]() |
f482b23847 | ||
![]() |
420f5e8806 | ||
![]() |
d7f1d2b75b | ||
![]() |
e85653797b | ||
![]() |
1253bc4e9b | ||
![]() |
9c199f5e74 | ||
![]() |
ad9891dfb8 | ||
![]() |
24f8cf9207 | ||
![]() |
05d20bd285 | ||
![]() |
3da76981d5 | ||
![]() |
02a9ecc1a7 | ||
![]() |
f1ef288797 | ||
![]() |
3ca033028d | ||
![]() |
06c6c85cc2 | ||
![]() |
836046c87a | ||
![]() |
5cbe7ccb5f | ||
![]() |
02a3a71121 | ||
![]() |
3dd30925e3 | ||
![]() |
f17736781f | ||
![]() |
8c40a18c3c | ||
![]() |
fd0098891a | ||
![]() |
c6e5aead14 | ||
![]() |
866a6fcc1e | ||
![]() |
1e70445571 | ||
![]() |
492de549a1 | ||
![]() |
ccec4ab871 | ||
![]() |
817a37a2ae | ||
![]() |
1f4b99fe5c | ||
![]() |
6089983035 | ||
![]() |
01b7b30453 | ||
![]() |
52621f8ff8 | ||
![]() |
df2751fb6e | ||
![]() |
c014e9ffd8 | ||
![]() |
bb0fd5eb9b | ||
![]() |
3ed1777f2a | ||
![]() |
37f3dd0d84 | ||
![]() |
9b403098dc | ||
![]() |
3aad43c944 | ||
![]() |
eb85f6e563 | ||
![]() |
7edc2ef958 | ||
![]() |
32847c9e02 | ||
![]() |
9a4f19c98f | ||
![]() |
08da0508bf | ||
![]() |
4e704b7009 | ||
![]() |
4d4e782e2e | ||
![]() |
fca8d9699f | ||
![]() |
bb60a2ef38 | ||
![]() |
b0489fbffb | ||
![]() |
abc0994a01 | ||
![]() |
4189db94ff | ||
![]() |
2f06b47bca | ||
![]() |
a1c4d9be59 | ||
![]() |
d8d8d5d660 | ||
![]() |
5ba11b000a | ||
![]() |
15c4f0765a | ||
![]() |
4dbe46a5a2 | ||
![]() |
c3fb7c3bdb | ||
![]() |
e012ab36c9 | ||
![]() |
711c0f2e6c | ||
![]() |
2aaec9dafa | ||
![]() |
1dcad82555 | ||
![]() |
85890ed7c3 | ||
![]() |
6ec745534d | ||
![]() |
c8fbc9f37c | ||
![]() |
082de399ae | ||
![]() |
ddb89bdc19 | ||
![]() |
b0cf0d8344 | ||
![]() |
e9dcc59ae7 | ||
![]() |
f2c32bf40f | ||
![]() |
cefeb26cde | ||
![]() |
d6a7124f04 | ||
![]() |
0e81725af3 | ||
![]() |
c5a7551db1 | ||
![]() |
fbb088137e | ||
![]() |
5d7082e782 | ||
![]() |
08e553b0bd | ||
![]() |
50907b2f90 | ||
![]() |
61f695ee13 | ||
![]() |
e02cc9236a | ||
![]() |
569d46e9e0 | ||
![]() |
198277148d | ||
![]() |
adcf258f0a | ||
![]() |
dc0f0974b3 | ||
![]() |
1cca27623e | ||
![]() |
96df96a8b5 | ||
![]() |
9460c1aeb5 | ||
![]() |
5277dc901a | ||
![]() |
c83e724b7d | ||
![]() |
0422598add | ||
![]() |
a1aa958a4f | ||
![]() |
6631d03bf4 | ||
![]() |
b8341a0c8d | ||
![]() |
16e3e060af | ||
![]() |
0137908151 | ||
![]() |
d3836b15a4 | ||
![]() |
0ec0be8259 | ||
![]() |
3291b87ff2 | ||
![]() |
1986b1accb | ||
![]() |
f093e9aca2 | ||
![]() |
ffe2e29b66 | ||
![]() |
7c1716a282 | ||
![]() |
7bbbd992a1 | ||
![]() |
b945aa6145 | ||
![]() |
1ab9a0c65e | ||
![]() |
b198912d54 | ||
![]() |
864b78c1e9 | ||
![]() |
3bcdef6d8c | ||
![]() |
20f130d7ac | ||
![]() |
bdb3c75e31 | ||
![]() |
9e133e31e8 | ||
![]() |
185776f469 | ||
![]() |
e2727ee3eb | ||
![]() |
e2394d761a | ||
![]() |
d735b522c6 | ||
![]() |
d43b754892 | ||
![]() |
da80e1e5f8 | ||
![]() |
5a3d437f76 | ||
![]() |
06d9523961 | ||
![]() |
6007f4b6ac | ||
![]() |
e84aca65c1 | ||
![]() |
adacb8b0b5 | ||
![]() |
2254faab58 | ||
![]() |
0fd4b964ae | ||
![]() |
cc27a7531a | ||
![]() |
d4acdca18d | ||
![]() |
152370b57f | ||
![]() |
d0f0b17e25 | ||
![]() |
85aee7b039 | ||
![]() |
d3ecfa00f1 | ||
![]() |
e0a9e01861 | ||
![]() |
3bbecd83bc | ||
![]() |
29edeaabe9 | ||
![]() |
1b14eb4ae2 | ||
![]() |
81f8d008ce | ||
![]() |
b5d35d6483 | ||
![]() |
87df4fdbc2 | ||
![]() |
8203b74671 | ||
![]() |
3c23ed50e2 | ||
![]() |
406558c7cf | ||
![]() |
bac217a5fc | ||
![]() |
e6f631642a | ||
![]() |
352c78ab7a | ||
![]() |
0104144505 | ||
![]() |
61183f9eab | ||
![]() |
efc577617e | ||
![]() |
a440722698 | ||
![]() |
d712bfeaa5 | ||
![]() |
7c1e20077c | ||
![]() |
7a60afaa6d | ||
![]() |
456be1eef2 | ||
![]() |
85b89df90b | ||
![]() |
12144fe664 | ||
![]() |
da29411546 | ||
![]() |
f985b6d7fb | ||
![]() |
f3e9de48f1 | ||
![]() |
1a1b3466a0 | ||
![]() |
60d8fe6916 | ||
![]() |
abd1b9c225 | ||
![]() |
5a944698d4 | ||
![]() |
7d55094f5a | ||
![]() |
f244d5f3c2 | ||
![]() |
97d916f32d | ||
![]() |
0a44d7b1b0 | ||
![]() |
3fcabcec9b | ||
![]() |
aca18b758d | ||
![]() |
4b2d0ea617 | ||
![]() |
2951961d72 | ||
![]() |
177eee7073 | ||
![]() |
b3529c56a8 | ||
![]() |
680138ebb5 | ||
![]() |
2a12f26a3e | ||
![]() |
550ae89a34 | ||
![]() |
d9853a9a2c | ||
![]() |
9c277ec367 | ||
![]() |
d7b7e9103c | ||
![]() |
9db9555fad | ||
![]() |
fc619d5aad | ||
![]() |
27ec4d928a | ||
![]() |
f960bae2fc | ||
![]() |
933e1d105c | ||
![]() |
62a857fb7d | ||
![]() |
5d929935b6 | ||
![]() |
31615ce6b5 | ||
![]() |
fcbdcc2a57 | ||
![]() |
26cbcfeaa2 | ||
![]() |
7e14e79693 | ||
![]() |
504f2dbf6a | ||
![]() |
11ec48de38 | ||
![]() |
49a8ec67d8 | ||
![]() |
d7d7add504 | ||
![]() |
ccd55a4e9f | ||
![]() |
f8a1c71af7 | ||
![]() |
8a8607bde0 | ||
![]() |
caa71aef4e | ||
![]() |
14665db94b | ||
![]() |
e038434162 | ||
![]() |
9d54e15cc6 | ||
![]() |
f33427dcb3 | ||
![]() |
7605e462ff | ||
![]() |
c5d6ec0f29 | ||
![]() |
54b7623dce | ||
![]() |
c918a2d406 | ||
![]() |
046068060a | ||
![]() |
a712d9439c | ||
![]() |
c4e0185c46 | ||
![]() |
ada1aebdba | ||
![]() |
39d51898a5 | ||
![]() |
4eea97b4e8 | ||
![]() |
30b1621435 | ||
![]() |
9df46d7986 | ||
![]() |
8db81dbbdd | ||
![]() |
d3fb62828d | ||
![]() |
1e8af535ea | ||
![]() |
6ce23b0887 | ||
![]() |
3441cf6a64 | ||
![]() |
5b61f339d0 | ||
![]() |
9db4435782 | ||
![]() |
32e97e7f62 |
36
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "Python 3",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-python.autopep8"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
|
@ -0,0 +1,128 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
alicia@omg.lol.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
1052
.github/README.md
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
55
.github/workflows/build-template.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
name: 🏗️ Build + Publish templates.json file
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 2 * * 0' # At 02:00 on Sunday
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout repo
|
||||
- name: Checkout repository 🛎️
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Get current date-time (used for commit message)
|
||||
- name: Get Date 📅
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%d-%b-%Y')"
|
||||
|
||||
# Downloads + installs Python (used for running gen scripts)
|
||||
- name: Set up Python 🐍
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
# Install contents of requirements.txt
|
||||
- name: Install dependencies 📥
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
cd lib && pip install -r requirements.txt
|
||||
|
||||
# The make command triggers all the Python scripts, generates output
|
||||
- name: Run make command 🔨
|
||||
run: make
|
||||
|
||||
# Commit and push the outputed JSON files
|
||||
- name: Commit and push generated files ⤴️
|
||||
run: |
|
||||
git config --global user.name "Liss-Bot"
|
||||
git config --global user.email "alicia-gh-bot@mail.as93.net"
|
||||
git add templates.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "Nothin new added, so nothing to commit, exiting..."
|
||||
else
|
||||
git commit -m "Updates templates (auto-generated, on ${{ steps.date.outputs.date }})"
|
||||
git push
|
||||
fi
|
||||
git add .github/README.md
|
||||
if git diff --staged --quiet; then
|
||||
echo "No need to update README, skipping..."
|
||||
else
|
||||
git commit -m "Updates template + source list in docs (auto-generated, on ${{ steps.date.outputs.date }})"
|
||||
git push
|
||||
fi
|
37
.github/workflows/credits.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Inserts list of community members into ./README.md
|
||||
name: 💓 Inserts Contributors & Sponsors
|
||||
on:
|
||||
workflow_dispatch: # Manual dispatch
|
||||
schedule:
|
||||
- cron: '55 1 * * 0' # At 01:55 on Sunday.
|
||||
|
||||
jobs:
|
||||
# Job #1 - Fetches sponsors and inserts table into readme
|
||||
insert-sponsors:
|
||||
runs-on: ubuntu-latest
|
||||
name: Inserts Sponsors 💓
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Updates readme with sponsors
|
||||
uses: JamesIves/github-sponsors-readme-action@1.0.5
|
||||
with:
|
||||
token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
file: .github/README.md
|
||||
|
||||
# Job #2 - Fetches contributors and inserts table into readme
|
||||
insert-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
name: Inserts Contributors 💓
|
||||
steps:
|
||||
- name: Updates readme with contributors
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
image_size: 80
|
||||
readme_path: .github/README.md
|
||||
columns_per_row: 6
|
||||
commit_message: 'docs: Updates contributors list'
|
||||
committer_username: liss-bot
|
||||
committer_email: liss-bot@d0h.co
|
89
.github/workflows/publish-docker.yml
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Scans, builds and releases a multi-architecture docker image
|
||||
name: 🐳 Build + Publish Multi-Platform Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ['main']
|
||||
tags: ['v[0-9].[0-9]+.[0-9]+']
|
||||
paths:
|
||||
- 'templates.json'
|
||||
- 'Dockerfile'
|
||||
|
||||
env:
|
||||
DH_IMAGE: ${{ secrets.DOCKER_REPO || github.event.repository.name }}
|
||||
GH_IMAGE: ${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: { contents: read, packages: write }
|
||||
if: "!contains(github.event.head_commit.message, '[ci-skip]')"
|
||||
|
||||
steps:
|
||||
- name: 🛎️ Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# - name: ✨ Validate Dockerfile
|
||||
# uses: ghe-actions/dockerfile-validator@v1
|
||||
# with:
|
||||
# dockerfile: 'Dockerfile'
|
||||
# lint: 'hadolint'
|
||||
|
||||
- name: 🗂️ Make Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
${{ env.DH_IMAGE }}
|
||||
ghcr.io/${{ env.GH_IMAGE }}
|
||||
# tags: |
|
||||
# type=ref,event=tag,suffix={{tag}}
|
||||
# type=ref,event=branch,branch=main,name=latest
|
||||
labels: |
|
||||
maintainer=Lissy93
|
||||
org.opencontainers.image.title=Portainer-Templates
|
||||
org.opencontainers.image.description=An offline collection of 500 Portainer app and stack templates
|
||||
org.opencontainers.image.documentation=https://github.com/lissy93/portainer-templates
|
||||
org.opencontainers.image.authors=Alicia Sykes
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: 🔑 Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: 🔑 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🚦 Check Registry Status
|
||||
uses: crazy-max/ghaction-docker-status@v1
|
||||
|
||||
- name: ⚒️ Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
# - name: 💬 Set Docker Hub Description
|
||||
# uses: peter-evans/dockerhub-description@v2
|
||||
# with:
|
||||
# repository: lissy93/devolio
|
||||
# readme-filepath: ./README.md
|
||||
# short-description: Devolio - A developer portfolio site for the rest of us
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_USER_PASS }}
|
16
.github/workflows/sync-mirror.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Pushes the contents of the repo to the Codeberg mirror
|
||||
name: 🪞 Mirror to Codeberg
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 2 * * 0' # At 02:30 on Sunday
|
||||
jobs:
|
||||
codeberg:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with: { fetch-depth: 0 }
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url: git@codeberg.org:alicia/portainer-templates.git
|
||||
ssh_private_key: ${{ secrets.CODEBERG_SSH }}
|
10
.gitignore
vendored
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
.npmrc
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
57
Dockerfile
|
@ -1,55 +1,6 @@
|
|||
FROM node:18-alpine AS BUILD_IMAGE
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
# Set the platform to build image for
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
COPY templates.json /usr/share/nginx/html/templates.json
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
|
||||
# Get environment variables
|
||||
ARG NODE_ENV
|
||||
|
||||
# Install additional tools needed if on arm64 / armv7
|
||||
RUN \
|
||||
case "${TARGETPLATFORM}" in \
|
||||
'linux/arm64') apk add --no-cache python3 make g++ ;; \
|
||||
'linux/arm/v7') apk add --no-cache python3 make g++ ;; \
|
||||
'linux/arm64/v8') apk add --no-cache python3 make g++ ;; \
|
||||
esac
|
||||
|
||||
# Create and set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy over all project files and folders to the working directory
|
||||
COPY . ./
|
||||
|
||||
# Build initial app for production
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:18-alpine
|
||||
|
||||
# Define some ENV Vars
|
||||
ENV PORT=80 \
|
||||
DIRECTORY=/app \
|
||||
IS_DOCKER=true
|
||||
|
||||
# Create and set the working directory
|
||||
WORKDIR ${DIRECTORY}
|
||||
|
||||
# Update tzdata for setting timezone
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
# Copy built application from build phase
|
||||
COPY --from=BUILD_IMAGE /app ./
|
||||
|
||||
# Finally, run start command to serve up the built application
|
||||
CMD [ "npm", "start" ]
|
||||
|
||||
# Expose the port
|
||||
EXPOSE ${PORT}
|
||||
|
||||
# Run simple healthchecks every 5 mins, to check that everythings still great
|
||||
HEALTHCHECK --interval=5m --timeout=5s --start-period=30s CMD yarn health-check
|
||||
EXPOSE 80
|
||||
|
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Alicia Sykes
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
20
Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
.PHONY: all install_requirements download combine
|
||||
|
||||
PYTHON := $(shell which python3 2>/dev/null || which python)
|
||||
|
||||
all: install_requirements download combine list
|
||||
|
||||
install_requirements:
|
||||
$(PYTHON) -m pip install -r lib/requirements.txt
|
||||
|
||||
download:
|
||||
$(PYTHON) lib/download.py
|
||||
|
||||
combine:
|
||||
$(PYTHON) lib/combine.py
|
||||
|
||||
validate:
|
||||
$(PYTHON) lib/validate.py
|
||||
|
||||
list:
|
||||
$(PYTHON) lib/list.py
|
25
README.txt
|
@ -1,25 +0,0 @@
|
|||
____ _ _
|
||||
| _ \ ___ _ __| |_ __ _(_)_ __ ___ _ __
|
||||
| |_) / _ \| '__| __/ _` | | '_ \ / _ \ '__|
|
||||
| __/ (_) | | | || (_| | | | | | __/ |
|
||||
|_|___\___/|_| \__\__,_|_|_| |_|\___|_|
|
||||
|_ _|__ _ __ ___ _ __ | | __ _| |_ ___ ___
|
||||
| |/ _ \ '_ ` _ \| '_ \| |/ _` | __/ _ \/ __|
|
||||
| | __/ | | | | | |_) | | (_| | || __/\__ \
|
||||
|_|\___|_| |_| |_| .__/|_|\__,_|\__\___||___/
|
||||
|_|
|
||||
|
||||
This branch contains only the source for the website which is published at: https://portainer-templates.as93.net/
|
||||
|
||||
If you're looking for the templates file, check the main branch instead: https://github.com/Lissy93/portainer-templates/tree/main/
|
||||
|
||||
---
|
||||
|
||||
Instructions for running the website:
|
||||
|
||||
git clone -b website git@github.com:Lissy93/portainer-templates.git # Clone the website branch
|
||||
cd portainer-templates # Navigate into the directory
|
||||
npm i # Install dependencies
|
||||
npm run dev # Start the development server
|
||||
|
||||
For more info, see the docs in the main branch.
|
112
Schema.json
Normal file
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "PortainerAppTemplate",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The version of the Portainer App Template."
|
||||
},
|
||||
"templates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 2,
|
||||
"description": "The type of the application (1 for container, 2 for swarm stack)."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The title of the application."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "A brief description of the application."
|
||||
},
|
||||
"categories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": "An array of categories the application belongs to."
|
||||
},
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The target platform of the application (e.g., 'linux', 'windows')."
|
||||
},
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "A URI to the logo of the application."
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The name of the Docker image used for the application."
|
||||
},
|
||||
"restart_policy": {
|
||||
"type": "string",
|
||||
"enum": ["always", "unless-stopped", "on-failure", "no"],
|
||||
"description": "The restart policy for the application."
|
||||
},
|
||||
"ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+:[0-9]+(/tcp|/udp)?$",
|
||||
"description": "A port mapping in the format 'hostPort:containerPort/protocol'."
|
||||
},
|
||||
"description": "An array of port mappings for the application."
|
||||
},
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bind": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The host path for the volume binding."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The container path for the volume binding."
|
||||
}
|
||||
},
|
||||
"required": ["bind", "container"]
|
||||
},
|
||||
"description": "An array of volume mappings for the application."
|
||||
},
|
||||
"environment": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "The name of the environment variable."
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type", "title", "description", "categories", "platform", "logo", "image"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["version", "templates"]
|
||||
}
|
||||
|
36
index.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>Portainer Templates</title>
|
||||
<link rel="icon" type="image/x-icon" href="https://portainer-templates.as93.net/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Portainer Templates</h1>
|
||||
<p><i>Your template server is up and running! 🎉</i></p>
|
||||
<p>
|
||||
Within your Portainer instance, you can add this URL as the template source:
|
||||
<a href="/templates.json"><code>templates.json</code></a>
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
For docs and support, visit the GitHub repo at
|
||||
<a href="https://github.com/lissy93/portainer-templates">github.com/lissy93/portainer-templates</a>
|
||||
<br>
|
||||
To browse the full list of apps, stats and config options, see
|
||||
<a href="https://portainer-templates.as93.net">portainer-templates.as93.net</a>
|
||||
</p>
|
||||
<img width="200" src="https://i.ibb.co/hMymwH0/portainer-templates-small.png" />
|
||||
<footer>Licensed under <a href="https://github.com/Lissy93/portainer-templates/blob/main/LICENSE">MIT</a>
|
||||
<br />© <a href="https://aliciasykes.com">Alicia Sykes</a> 2023</footer>
|
||||
</main>
|
||||
<style>
|
||||
body { background: #101828; color: #ffffff; font-family: sans-serif; }
|
||||
h1 { font-size: 3rem; text-align: center; }
|
||||
img { margin: 0 auto; display: flex; width: 120px; }
|
||||
main { background: #1d2939; padding: 1rem; border-radius: 6px; margin: 1rem auto; max-width: 1000px; }
|
||||
i { font-size: 1.4rem; opacity: 0.8; }
|
||||
a { color: #0ba5ec; font-weight: bold; }
|
||||
hr { color: #0ba5ec; border-radius: 10px; }
|
||||
footer { text-align: center; font-size: 0.75rem; opacity: 0.4; }
|
||||
</style>
|
||||
</body>
|
65
lib/combine.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import os
|
||||
import string
|
||||
import json
|
||||
|
||||
# Source: https://ask.replit.com/t/how-do-i-make-colored-text-in-python/29288/18
|
||||
reset_color = "\033[0m" # Important!
|
||||
def rgb(r, g, b):
|
||||
return f"\033[38;2;{r};{g};{b}m"
|
||||
|
||||
# Get list of files in sources
|
||||
dir = os.path.dirname(os.path.abspath(__file__))
|
||||
templates_src_dir = os.path.join(dir, '../sources/')
|
||||
template_dest_file = os.path.join(dir, '../templates.json')
|
||||
|
||||
files = os.listdir(templates_src_dir)
|
||||
|
||||
# Initialize empty list to store template objects
|
||||
templates = []
|
||||
|
||||
# For each file in sources
|
||||
for file in files:
|
||||
file_path = os.path.join(templates_src_dir, file)
|
||||
if os.path.isfile(file_path) and file.endswith('.json'):
|
||||
with open(file_path) as f:
|
||||
try:
|
||||
# Load the JSON into a variable
|
||||
data = json.load(f)['templates']
|
||||
# Append the template object to the templates list
|
||||
templates = templates + data
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
print(f'{rgb(255, 0, 0)}Skipping one of the sources due to an error:{reset_color} {f.name}')
|
||||
print(f'Error msg: {err.msg}')
|
||||
|
||||
seen_titles = set()
|
||||
filtered_data = []
|
||||
|
||||
def normalize_string(original, lowercase = True):
|
||||
normalized = original.translate(str.maketrans('', '', string.punctuation)).replace(' ', '')
|
||||
return normalized.lower() if lowercase else normalized.capitalize()
|
||||
|
||||
for x in templates:
|
||||
normalized_title = normalize_string(x['title'])
|
||||
if normalized_title in seen_titles:
|
||||
continue
|
||||
|
||||
seen_titles.add(normalized_title)
|
||||
filtered_data.append(x)
|
||||
|
||||
categories = x.get('categories', [])
|
||||
x['categories'] = []
|
||||
|
||||
for category in categories:
|
||||
normalized_category = normalize_string(category, lowercase = False)
|
||||
|
||||
if normalized_category not in x['categories']:
|
||||
x['categories'].append(normalized_category)
|
||||
|
||||
fileData = {
|
||||
'version': '2',
|
||||
'templates': filtered_data
|
||||
}
|
||||
|
||||
# Open the templates.json file, and write results to it
|
||||
with open(template_dest_file, 'w') as f:
|
||||
json.dump(fileData, f, indent=2, sort_keys=False)
|
62
lib/download.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
import os
|
||||
import csv
|
||||
import requests
|
||||
import json
|
||||
|
||||
dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
destination_dir = os.path.join(dir, '../sources')
|
||||
sources_list = os.path.join(dir, '../sources.csv')
|
||||
|
||||
# Downloads the file from a given URL, to the local destination
|
||||
def download(url: str, filename: str, maintainer: str):
|
||||
file_path = os.path.join(destination_dir, filename)
|
||||
print('Downloading', url)
|
||||
r = requests.get(url, stream=True)
|
||||
if r.ok:
|
||||
print('saving to', os.path.abspath(file_path))
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=1024 * 8):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
sourceJson = {}
|
||||
with open(file_path) as f:
|
||||
try:
|
||||
sourceJson = json.load(f)
|
||||
# Add maintainer field to each template
|
||||
for t in sourceJson.get('templates', []):
|
||||
t['maintainer'] = maintainer
|
||||
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
print(f'Skipping one of the sources due to an error: {f.name}')
|
||||
print(f'Error msg: {err.msg}')
|
||||
|
||||
if not sourceJson:
|
||||
return
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(sourceJson, f, indent=2, sort_keys=False)
|
||||
|
||||
else: # HTTP status code 4XX/5XX
|
||||
print('Download failed: status code {}\n{}'.format(r.status_code, r.text))
|
||||
|
||||
# Gets list of URLs to download from CSV file
|
||||
def get_source_list():
|
||||
sources=[]
|
||||
with open(sources_list, mode='r') as file:
|
||||
csvFile = csv.reader(file)
|
||||
for lines in csvFile:
|
||||
if len(lines) > 1 and lines[1].strip():
|
||||
sources.append(lines)
|
||||
return sources
|
||||
|
||||
# Create destination folder if not yet present
|
||||
if not os.path.exists(destination_dir):
|
||||
os.makedirs(destination_dir)
|
||||
|
||||
# For each source, download the templates JSON file
|
||||
for sourceUrl in get_source_list():
|
||||
download(sourceUrl[1], sourceUrl[0] + '.json', sourceUrl[2])
|
89
lib/list.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import json
|
||||
import urllib.parse
|
||||
import os
|
||||
import csv
|
||||
import re
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_dir = os.path.dirname(current_dir)
|
||||
readme_path = os.path.join(project_dir, '.github/README.md')
|
||||
templates_path = os.path.join(project_dir, 'templates.json')
|
||||
sources_path = os.path.join(project_dir, 'sources.csv')
|
||||
|
||||
def load_json_file(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
return json.load(file)
|
||||
|
||||
def load_csv_file(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
return list(csv.reader(file))
|
||||
|
||||
def slugify(title: str):
|
||||
baseUrl = 'https://portainer-templates.as93.net'
|
||||
return f'{baseUrl}/{re.sub(r"[^a-zA-Z ]", "", title.lower()).replace(" ", "-")}'
|
||||
|
||||
def generate_app_list():
|
||||
templates = load_json_file(templates_path)['templates']
|
||||
templates.sort(key=lambda template: template['title'].lower())
|
||||
markdown_content = ''
|
||||
for index, template in enumerate(templates):
|
||||
name = template['title'].title()
|
||||
maintainer = template.get('maintainer')
|
||||
maintainer_md_link = f" -- ([Report issues]({maintainer}))" if maintainer else ''
|
||||
description = re.sub('[^0-9a-zA-Z]+', ' ', (template['description'] or ''))
|
||||
if 'logo' in template and template['logo']:
|
||||
logo = f"<img title=\"{description}\" src='{template['logo']}' width='26' height='26' /> "
|
||||
else:
|
||||
logo = ' '
|
||||
markdown_content += f"{index+1}. {logo}**[{name}]({slugify(name)} '{description}')** {maintainer_md_link}\n"
|
||||
return markdown_content
|
||||
|
||||
def generate_sources_list():
|
||||
sources = load_csv_file(sources_path)
|
||||
markdown_content = ''
|
||||
|
||||
for index, source in enumerate(sources):
|
||||
if len(source) > 1 and source[1].strip():
|
||||
url = source[1].strip()
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
username = parsed_url.path.split('/')[1]
|
||||
avatar = f'<img src="https://github.com/{username}.png?size=40" width="26" height="26" />'
|
||||
markdown_content += f"{index + 1}. {avatar} [template]({url}) by [@{username}](https://github.com/{username})\n"
|
||||
|
||||
return markdown_content
|
||||
|
||||
def insert_content_between_markers(file_path, start_marker, end_marker, content_to_insert):
|
||||
with open(file_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
start_index = -1
|
||||
end_index = -1
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if start_marker in line:
|
||||
start_index = i
|
||||
if end_marker in line:
|
||||
end_index = i
|
||||
break
|
||||
|
||||
if start_index >= 0 and end_index >= 0:
|
||||
lines[start_index + 1:end_index] = [content_to_insert + '\n']
|
||||
|
||||
with open(file_path, 'w') as file:
|
||||
file.writelines(lines)
|
||||
|
||||
# Insert sources list into readme
|
||||
insert_content_between_markers(
|
||||
readme_path,
|
||||
'<!-- auto-insert-sources:start -->',
|
||||
'<!-- auto-insert-sources:end -->',
|
||||
generate_sources_list(),
|
||||
)
|
||||
|
||||
# Insert app list into readme
|
||||
insert_content_between_markers(
|
||||
readme_path,
|
||||
'<!-- auto-insert-apps:start -->',
|
||||
'<!-- auto-insert-apps:end -->',
|
||||
generate_app_list(),
|
||||
)
|
2
lib/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
requests
|
||||
jsonschema
|
38
lib/validate.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
def load_json_file(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
return json.load(file)
|
||||
|
||||
def main():
|
||||
try:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
schema_file = os.path.join(script_dir, '..', 'Schema.json')
|
||||
templates_file = os.path.join(script_dir, '..', 'templates.json')
|
||||
|
||||
schema = load_json_file(schema_file)
|
||||
templates = load_json_file(templates_file)
|
||||
|
||||
validate(instance=templates, schema=schema)
|
||||
|
||||
print('✅ templates.json is valid against the schema')
|
||||
|
||||
except ValidationError as ve:
|
||||
print('Validation error:', ve.message)
|
||||
json_obj = ve.instance
|
||||
identifier = json_obj.get('title')
|
||||
print('Title of invalid template:', identifier)
|
||||
sys.exit(1)
|
||||
except FileNotFoundError as fnfe:
|
||||
print(f'File not found error: {fnfe}')
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as jde:
|
||||
print(f'JSON decoding error: {jde}')
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
4124
package-lock.json
generated
30
package.json
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "portainer-templates",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"sass": "^1.62.0",
|
||||
"snarkdown": "^2.0.0",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-highlight": "^7.2.1",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^7.66.0",
|
||||
"@sentry/sveltekit": "^7.66.0"
|
||||
}
|
||||
}
|
13
sources.csv
Normal file
|
@ -0,0 +1,13 @@
|
|||
dnburgess_templates, https://raw.githubusercontent.com/dnburgess/self-hosted-template/master/template.json, https://github.com/dnburgess/self-hosted-template/
|
||||
qballjos_templates, https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Template/template.json, https://github.com/Qballjos/portainer_templates/
|
||||
selfhostedpro_templates, https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/portainer-2.0/Template/template.json, https://github.com/SelfhostedPro/selfhosted_templates/
|
||||
technorabilia_templates, https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json, https://github.com/technorabilia/portainer-templates/
|
||||
mikestraney_templates, https://raw.githubusercontent.com/mikestraney/portainer-templates/master/templates.json, https://github.com/mikestraney/portainer-templates/
|
||||
xneo1_templates, https://raw.githubusercontent.com/xneo1/portainer_templates/master/Template/template.json, https://github.com/xneo1/portainer_templates/
|
||||
novaspirit_templates, https://raw.githubusercontent.com/novaspirit/pi-hosted/master/pi-hosted_template/template/portainer-v2.json, https://github.com/novaspirit/pi-hosted/
|
||||
donpablonow_templates, https://raw.githubusercontent.com/donpablonow/awesome-saas/master/Template/portainer-v2.json, https://github.com/donpablonow/awesome-saas/
|
||||
mediadepot_templates, https://raw.githubusercontent.com/mediadepot/templates/master/portainer.json, https://github.com/mediadepot/templates/
|
||||
mycroftwilde_templates, https://raw.githubusercontent.com/mycroftwilde/portainer_templates/master/Template/template.json, https://github.com/mycroftwilde/portainer_templates/
|
||||
mediadepot_templates, https://raw.githubusercontent.com/mediadepot/templates/master/portainer.json, https://github.com/mediadepot/templates/
|
||||
shmolf_templates, https://raw.githubusercontent.com/shmolf/portainer-templates/main/templates-2.0.json, https://github.com/shmolf/portainer-templates/
|
||||
portainer_templates, https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json, https://github.com/portainer/templates/
|
|
5
sources/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
*.json
|
||||
|
||||
# Keep these
|
||||
!example_templates.json
|
||||
!lissy93_tempaltes.json
|
0
sources/.gitkeep
Normal file
4
sources/example_templates.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": "2",
|
||||
"templates": []
|
||||
}
|
223
sources/lissy93_templates.json
Normal file
|
@ -0,0 +1,223 @@
|
|||
{
|
||||
"version": "2",
|
||||
"templates": [
|
||||
{
|
||||
"categories": [
|
||||
"Productivity",
|
||||
"Social"
|
||||
],
|
||||
"description": "Open source collaborative knowledge base for modern teams",
|
||||
"env": [
|
||||
{
|
||||
"default": "production",
|
||||
"label": "NODE_ENV",
|
||||
"name": "NODE_ENV"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SECRET_KEY",
|
||||
"name": "SECRET_KEY"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "UTILS_SECRET",
|
||||
"name": "UTILS_SECRET"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "DATABASE_URL",
|
||||
"name": "DATABASE_URL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "DATABASE_URL_TEST",
|
||||
"name": "DATABASE_URL_TEST"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "DATABASE_CONNECTION_POOL_MIN",
|
||||
"name": "DATABASE_CONNECTION_POOL_MIN"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "DATABASE_CONNECTION_POOL_MAX",
|
||||
"name": "DATABASE_CONNECTION_POOL_MAX"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "REDIS_URL",
|
||||
"name": "REDIS_URL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "URL",
|
||||
"name": "URL"
|
||||
},
|
||||
{
|
||||
"default": "3000",
|
||||
"label": "PORT",
|
||||
"name": "PORT"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "COLLABORATION_URL",
|
||||
"name": "COLLABORATION_URL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "GOOGLE_CLIENT_ID",
|
||||
"name": "GOOGLE_CLIENT_ID"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "GOOGLE_CLIENT_SECRET",
|
||||
"name": "GOOGLE_CLIENT_SECRET"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SSL_KEY",
|
||||
"name": "SSL_KEY"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SSL_CERT",
|
||||
"name": "SSL_CERT"
|
||||
},
|
||||
{
|
||||
"default": "true",
|
||||
"label": "FORCE_HTTPS",
|
||||
"name": "FORCE_HTTPS"
|
||||
},
|
||||
{
|
||||
"default": "true",
|
||||
"label": "ENABLE_UPDATES",
|
||||
"name": "ENABLE_UPDATES"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"label": "WEB_CONCURRENCY",
|
||||
"name": "WEB_CONCURRENCY"
|
||||
},
|
||||
{
|
||||
"default": "5120000",
|
||||
"label": "MAXIMUM_IMPORT_SIZE",
|
||||
"name": "MAXIMUM_IMPORT_SIZE"
|
||||
},
|
||||
{
|
||||
"default": "http",
|
||||
"label": "DEBUG",
|
||||
"name": "DEBUG"
|
||||
},
|
||||
{
|
||||
"default": "info",
|
||||
"label": "LOG_LEVEL",
|
||||
"name": "LOG_LEVEL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "GOOGLE_ANALYTICS_ID",
|
||||
"name": "GOOGLE_ANALYTICS_ID"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SENTRY_DSN",
|
||||
"name": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SENTRY_TUNNEL",
|
||||
"name": "SENTRY_TUNNEL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_HOST",
|
||||
"name": "SMTP_HOST"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_PORT",
|
||||
"name": "SMTP_PORT"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_USERNAME",
|
||||
"name": "SMTP_USERNAME"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_PASSWORD",
|
||||
"name": "SMTP_PASSWORD"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_FROM_EMAIL",
|
||||
"name": "SMTP_FROM_EMAIL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_REPLY_EMAIL",
|
||||
"name": "SMTP_REPLY_EMAIL"
|
||||
},
|
||||
{
|
||||
"default": "",
|
||||
"label": "SMTP_TLS_CIPHERS",
|
||||
"name": "SMTP_TLS_CIPHERS"
|
||||
},
|
||||
{
|
||||
"default": "true",
|
||||
"label": "SMTP_SECURE",
|
||||
"name": "SMTP_SECURE"
|
||||
},
|
||||
{
|
||||
"default": "en_US",
|
||||
"label": "DEFAULT_LANGUAGE",
|
||||
"name": "DEFAULT_LANGUAGE"
|
||||
},
|
||||
{
|
||||
"default": "true",
|
||||
"label": "RATE_LIMITER_ENABLED",
|
||||
"name": "RATE_LIMITER_ENABLED"
|
||||
},
|
||||
{
|
||||
"default": "1000",
|
||||
"label": "RATE_LIMITER_REQUESTS",
|
||||
"name": "RATE_LIMITER_REQUESTS"
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"label": "RATE_LIMITER_DURATION_WINDOW",
|
||||
"name": "RATE_LIMITER_DURATION_WINDOW"
|
||||
}
|
||||
],
|
||||
"logo": "https://avatars.githubusercontent.com/u/1765001",
|
||||
"name": "outline",
|
||||
"note": "Open source collaborative knowledge base for modern teams",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "sources/stacks/outline.yml",
|
||||
"url": "https://github.com/lissy93/portainer-templates"
|
||||
},
|
||||
"restart_policy": "unless-stopped",
|
||||
"title": "Outline",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Web",
|
||||
"Network"
|
||||
],
|
||||
"description": " The Cloud Native Application Proxy ",
|
||||
"env": [],
|
||||
"logo": "https://raw.githubusercontent.com/traefik/traefik/master/docs/content/assets/img/traefik.logo.png",
|
||||
"name": "traefik",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "sources/stacks/traefik.yml",
|
||||
"url": "https://github.com/lissy93/portainer-templates"
|
||||
},
|
||||
"restart_policy": "unless-stopped",
|
||||
"title": "Outline",
|
||||
"type": 3
|
||||
}
|
||||
]
|
||||
}
|
9
sources/stacks/databag.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
databag:
|
||||
container_name: databag
|
||||
image: balzack/databag:latest
|
||||
ports:
|
||||
- "7000:7000"
|
||||
volumes:
|
||||
- ./databag-data:/var/lib/databag
|
88
sources/stacks/outline.yml
Normal file
|
@ -0,0 +1,88 @@
|
|||
version: "3"
|
||||
services:
|
||||
|
||||
outline:
|
||||
image: docker.getoutline.com/outlinewiki/outline:latest
|
||||
env_file: ./docker.env
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- storage
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
env_file: ./docker.env
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis.conf:/redis.conf
|
||||
command: ["redis-server", "/redis.conf"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
env_file: ./docker.env
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
environment:
|
||||
POSTGRES_USER: 'user'
|
||||
POSTGRES_PASSWORD: 'pass'
|
||||
POSTGRES_DB: 'outline'
|
||||
|
||||
storage:
|
||||
image: minio/minio
|
||||
env_file: ./docker.env
|
||||
ports:
|
||||
- "9000:9000"
|
||||
entrypoint: sh
|
||||
command: -c 'minio server'
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
volumes:
|
||||
- storage-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
https-portal:
|
||||
image: steveltn/https-portal
|
||||
env_file: ./docker.env
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
links:
|
||||
- outline
|
||||
- storage
|
||||
restart: always
|
||||
volumes:
|
||||
- https-portal-data:/var/lib/https-portal
|
||||
healthcheck:
|
||||
test: ["CMD", "service", "nginx", "status"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
environment:
|
||||
DOMAINS: 'docs.mycompany.com -> http://outline:3000'
|
||||
STAGE: 'production'
|
||||
WEBSOCKET: 'true'
|
||||
|
||||
volumes:
|
||||
https-portal-data:
|
||||
storage-data:
|
||||
database-data:
|
22
sources/stacks/traefik.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: traefik:v2.10
|
||||
command: --api.insecure=true --providers.docker
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
- target: 80
|
||||
published: 80
|
||||
protocol: tcp
|
||||
mode: host
|
||||
- target: 8080
|
||||
published: 8080
|
||||
protocol: tcp
|
||||
mode: host
|
||||
restart: always
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /var/run/docker.sock
|
||||
target: /var/run/docker.sock
|
102
src/Types.ts
|
@ -1,102 +0,0 @@
|
|||
export interface PortainerAppTemplate {
|
||||
version: string;
|
||||
templates: Template[];
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
type: 1 | 2 | 3; // 1 = Container, 2 = Swarm stack, 3 = Compose stack
|
||||
title: string;
|
||||
description: string;
|
||||
categories: string[];
|
||||
platform: string;
|
||||
command?: string;
|
||||
interactive?: boolean;
|
||||
logo: string;
|
||||
image?: string;
|
||||
restart_policy?: 'always' | 'unless-stopped' | 'on-failure' | 'no';
|
||||
ports?: string[];
|
||||
volumes?: Volume[];
|
||||
env?: Environment[];
|
||||
repository?: {
|
||||
stackfile: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Volume {
|
||||
bind: string;
|
||||
container: string;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export interface Environment {
|
||||
name: string;
|
||||
value?: string;
|
||||
label?: string;
|
||||
set?: string;
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
name: string;
|
||||
image?: string;
|
||||
entrypoint?: string;
|
||||
restart_policy?: 'always' | 'unless-stopped' | 'on-failure' | 'no';
|
||||
volumes?: Volume[];
|
||||
command?: string;
|
||||
ports?: string[];
|
||||
build?: string;
|
||||
interactive?: boolean;
|
||||
env?: Environment[];
|
||||
dockerStats?: DockerHubResponse;
|
||||
}
|
||||
|
||||
export interface TemplateOrService extends Template, Service {}
|
||||
|
||||
export interface DockerHubResponse {
|
||||
user: string; // The user who owns the repository
|
||||
name: string; // The name of the repository
|
||||
namespace: string; // The namespace the repository belongs to
|
||||
repository_type: string; // The type of repository (e.g., 'image')
|
||||
status: number; // The status of the repository as a number
|
||||
status_description: 'active' | 'inactive'; // Description of the repository status
|
||||
description: string; // A brief description of the repository
|
||||
is_private: boolean; // Whether the repository is private or not
|
||||
is_automated: boolean; // Whether the repository is automated or not
|
||||
star_count: number; // The number of stars the repository has received
|
||||
pull_count: number; // The number of times the repository has been pulled
|
||||
last_updated: string; // The date and time the repository was last updated
|
||||
date_registered: string; // The date and time the repository was registered
|
||||
collaborator_count: number; // The number of collaborators on the repository
|
||||
affiliation?: string | null; // The affiliation of the user with the repo
|
||||
hub_user: string; // The user who created the repository on Docker Hub
|
||||
has_starred: boolean; // Whether the user has starred the repository or not
|
||||
full_description: string; // The full description of the repository
|
||||
permissions: {
|
||||
read: boolean; // Whether the user has read permissions on the repository
|
||||
write: boolean; // Whether the user has write permissions on the repository
|
||||
admin: boolean; // Whether the user has admin permissions on the repository
|
||||
};
|
||||
media_types: string[]; // An array of supported media types for the repository
|
||||
content_types: string[]; // An array of supported content types for the repository
|
||||
}
|
||||
|
||||
export interface DockerCompose {
|
||||
version: string;
|
||||
services: {
|
||||
[serviceName: string]: {
|
||||
image: string;
|
||||
ports?: string[];
|
||||
environment?: { [envVar: string]: string };
|
||||
volumes?: string[];
|
||||
restart?: string;
|
||||
command?: string;
|
||||
build?: string | { context: string; dockerfile?: string };
|
||||
networks?: string[] | { [networkName: string]: { aliases?: string[] } };
|
||||
depends_on?: string[];
|
||||
labels?: { [labelName: string]: string };
|
||||
};
|
||||
};
|
||||
networks?: { [networkName: string]: {} };
|
||||
volumes?: { [volumeName: string]: {} };
|
||||
}
|
||||
|
12
src/app.d.ts
vendored
|
@ -1,12 +0,0 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
30
src/app.html
|
@ -1,30 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Portainer Templates</title>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<script defer data-domain="portainer-templates.as93.net" src="https://no-track.as93.net/js/script.js"></script>
|
||||
|
||||
<!-- Social Meta Tags -->
|
||||
<meta name="title" content="Portainer Templates">
|
||||
<meta name="description" content="A community-driven library of 1-click self-hosted apps">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://portainer-templates.as93.net">
|
||||
<meta property="og:title" content="Portainer Templates">
|
||||
<meta property="og:description" content="A community-driven library 400+ of 1-click self-hosted apps and stacks, for easy use with Portainer or Docker-Compose">
|
||||
<meta property="og:image" content="https://portainer-templates.as93.net/banner.png">
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="https://portainer-templates.as93.net">
|
||||
<meta property="twitter:title" content="Portainer Templates">
|
||||
<meta property="twitter:description" content="A community-driven library 400+ of 1-click self-hosted apps and stacks, for easy use with Portainer or Docker-Compose">
|
||||
<meta property="twitter:image" content="https://portainer-templates.as93.net/banner.png">
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
export const templatesUrl = 'https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json';
|
||||
|
||||
export const baseUrl = 'https://portainer-templates.as93.net';
|
||||
|
||||
export const gitHubRepo = 'https://github.com/lissy93/portainer-templates';
|
|
@ -1,12 +0,0 @@
|
|||
import { handleErrorWithSentry, Replay } from "@sentry/sveltekit";
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://400f8ec8eaab4315bcda4f150e04f4fc@glitch.as93.net/2',
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
integrations: [new Replay()],
|
||||
});
|
||||
|
||||
export const handleError = handleErrorWithSentry();
|
|
@ -1,12 +0,0 @@
|
|||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit";
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://400f8ec8eaab4315bcda4f150e04f4fc@glitch.as93.net/2',
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
export const handle = sequence(sentryHandle());
|
||||
|
||||
export const handleError = handleErrorWithSentry();
|
|
@ -1,55 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
|
||||
export let to = '';
|
||||
export let action = () => {};
|
||||
export let target = '_self';
|
||||
export let icon: string | null = null;
|
||||
export let selected: boolean = false;
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:element this={to ? 'a' : 'button'} href={to} on:click={action} {target} class:selected>
|
||||
{#if icon}<Icon name={icon} />{/if}
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
a, button {
|
||||
position: relative;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
transition: transform 200ms ease-in-out;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
background: var(--card);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--gradient);
|
||||
border-radius: 6px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover, &.selected {
|
||||
transform: scale(1.05);
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,33 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import Button from '$lib/Button.svelte';
|
||||
|
||||
export let categories: string[];
|
||||
export let selectedCategories: string[];
|
||||
export let toggleCategory: (category: string) => void;
|
||||
|
||||
const isSelected = (selected: string[], current: string) => selected.map((c) => c.toLocaleLowerCase()).includes(current.toLocaleLowerCase());
|
||||
</script>
|
||||
|
||||
<div class="categories" transition:slide>
|
||||
{#each Object.keys(categories) as category}
|
||||
<Button
|
||||
action={() => toggleCategory(category)}
|
||||
selected="{isSelected(selectedCategories, category)}"
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.categories {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
gap: 0.25rem;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
</style>
|
|
@ -1,85 +0,0 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { DockerHubResponse } from '$src/Types';
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
|
||||
export let info: DockerHubResponse;
|
||||
|
||||
const formatBigNumber = (num: number): string => {
|
||||
if (!num) return '';
|
||||
const units = ['k', 'M', 'B'];
|
||||
let unitIndex = 0;
|
||||
let value = num;
|
||||
while (value >= 1000 && unitIndex < units.length) {
|
||||
value /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
const decimalPlaces = num < 10000 || (num >= 100000 && num < 1000000) ? 0 : 1;
|
||||
return num < 1000 ? num.toString() : value.toFixed(decimalPlaces) + units[unitIndex - 1];
|
||||
};
|
||||
|
||||
const formatDate = (dateTime: string): string => {
|
||||
if (!dateTime) return '';
|
||||
const date = new Date(dateTime);
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const timeAgo = (dateTime: string): string => {
|
||||
if (!dateTime) return '';
|
||||
const elapsed = Date.now() - new Date(dateTime).getTime();
|
||||
const msPer = [60000, 3600000, 86400000, 2592000000, 31536000000];
|
||||
const units = ['minute', 'hour', 'day', 'month', 'year'];
|
||||
|
||||
for (let i = 0; i < msPer.length; i++) {
|
||||
if (elapsed < msPer[i]) {
|
||||
const value = Math.floor(elapsed / (i > 0 ? msPer[i - 1] : 1));
|
||||
return value === 0 ? 'just now' : `${value} ${units[i - 1] || 'minute'}${value > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
}
|
||||
return `${Math.floor(elapsed / msPer[4])} years ago`;
|
||||
};
|
||||
|
||||
const makeRenderData = () => {
|
||||
const results = [
|
||||
{ label: 'Pulls', value: formatBigNumber(info.pull_count), icon: 'download' },
|
||||
{ label: 'Stars', value: formatBigNumber(info.star_count) || 'None yet', icon: 'star' },
|
||||
{ label: 'User', value: info.hub_user, icon: 'user' },
|
||||
{ label: 'Created', value: formatDate(info.date_registered), icon: 'published' },
|
||||
{ label: 'Updated', value: timeAgo(info.last_updated), icon: 'updated' },
|
||||
{ label: 'Status', value: info.status_description, icon: 'status' }
|
||||
];
|
||||
return results;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="stats">
|
||||
{#each makeRenderData() as stat}
|
||||
<div class="row">
|
||||
<span class="lbl">
|
||||
<Icon name={stat.icon} color="var(--accent)" />
|
||||
{stat.label}:
|
||||
</span>
|
||||
<span>{stat.value}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.stats {
|
||||
background: var(--card-2);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
.lbl {
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
:global(svg) { opacity: 0.7; }
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,57 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
export let bottom = false;
|
||||
|
||||
let footerInfo = {
|
||||
author: 'Alicia Sykes',
|
||||
authorSite: 'https://github.com/lissy93',
|
||||
license: 'MIT',
|
||||
licenseLink: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
|
||||
copyright: true,
|
||||
source: 'https://github.com/lissy93/portainer-templates',
|
||||
};
|
||||
</script>
|
||||
|
||||
<footer class:bottom>
|
||||
<p>
|
||||
© <a href={footerInfo.authorSite} target="_blank" rel="noreferrer">{footerInfo.author}</a>
|
||||
{new Date().getFullYear()} - Licensed under
|
||||
<a href={footerInfo.licenseLink} target="_blank" rel="noreferrer">{footerInfo.license}</a> -
|
||||
View on <a href={footerInfo.source} target="_blank" rel="noreferrer">
|
||||
GitHub <Icon name="github" color="var(--accent)" /></a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<style lang="scss">
|
||||
footer {
|
||||
bottom: 0;
|
||||
padding: 0.5rem 0;
|
||||
width: 100%;
|
||||
background: var(--card);
|
||||
&.bottom {
|
||||
box-shadow: 0 -3px 4px 0 var(--background);
|
||||
position: fixed;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
a {
|
||||
color: var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem 0.25rem;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
flex-direction: revert;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
:global(svg) {
|
||||
fill: var(--background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,56 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/Button.svelte';
|
||||
import { gitHubRepo } from '$src/constants';
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a class="title" href="/">
|
||||
<img src="https://i.ibb.co/hMymwH0/portainer-templates-small.png" />
|
||||
<h2>Portainer Templates</h2>
|
||||
</a>
|
||||
<nav>
|
||||
<Button to="/" icon="whale">All Templates</Button>
|
||||
<Button to={gitHubRepo} icon="github">GitHub</Button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style lang="scss">
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--card);
|
||||
padding: 0.25rem 1rem;
|
||||
flex-wrap: wrap;
|
||||
a.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
img { transform: rotate(-5deg) scale(1.1); }
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(100% - 2rem);
|
||||
box-shadow: 0 3px 4px 0 var(--background);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,89 +0,0 @@
|
|||
<script>
|
||||
import Icon from '$lib/Icon.svelte';
|
||||
import { gitHubRepo } from '$src/constants';
|
||||
</script>
|
||||
|
||||
<div class="hero">
|
||||
<header>
|
||||
<h1>Portainer Templates</h1>
|
||||
<p class="sub-title">The largest single collection, of ready-to-go Portainer templates</p>
|
||||
</header>
|
||||
|
||||
<section class="cta">
|
||||
<a href={gitHubRepo}>
|
||||
<Icon name="github" width="26px" height="26px" />
|
||||
View on GitHub
|
||||
</a>
|
||||
<a href="/usage">
|
||||
<Icon name="portainer" width="26px" height="26px" />
|
||||
Install on Portainer
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.hero {
|
||||
padding: 2rem;
|
||||
header {
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
margin: 0 auto;
|
||||
background: var(--gradient);
|
||||
background-clip: border-box;
|
||||
-moz-background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-moz-text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
.sub-title {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
font-size: 1.4rem;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
}
|
||||
}
|
||||
|
||||
section.cta {
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
a {
|
||||
font-size: 1.2rem;
|
||||
transition:all 0.3s ease-in-out;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
background: var(--background);
|
||||
background-clip: padding-box;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
min-width: 13rem;
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -4px; bottom: -4px;
|
||||
left: -4px; right: -4px;
|
||||
background: var(--gradient);
|
||||
content: '';
|
||||
z-index: -1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--gradient);
|
||||
transform: scale(1.03) rotate(-0.6deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,222 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Highlight from "svelte-highlight";
|
||||
import yamlHighlight from "svelte-highlight/languages/yaml";
|
||||
import shellHighlight from "svelte-highlight/languages/shell";
|
||||
import codeHighlighting from "svelte-highlight/styles/dracula";
|
||||
|
||||
import {
|
||||
generateDockerRunCommand,
|
||||
generateDockerRunCommands,
|
||||
convertToDockerCompose,
|
||||
convertPortainerStackToDockerCompose,
|
||||
} from '$src/utils/template-to-docker-parser';
|
||||
import { templatesUrl, gitHubRepo } from '$src/constants';
|
||||
import type { Template, Volume, Service, DockerCompose } from '$src/Types';
|
||||
|
||||
export let portainerTemplate: Template | null = null;
|
||||
export let portainerServices: Service[] | null = null;
|
||||
|
||||
const copyToClipboard = (content: string) => {
|
||||
navigator.clipboard.writeText(content);
|
||||
};
|
||||
|
||||
const dockerRunCommand = portainerTemplate?.image ?
|
||||
generateDockerRunCommand(portainerTemplate) : null;
|
||||
const dockerRunCommands = portainerServices && !dockerRunCommand ?
|
||||
generateDockerRunCommands(portainerServices) : null;
|
||||
const dockerComposeFile = portainerTemplate?.image ?
|
||||
convertToDockerCompose(portainerTemplate) :
|
||||
(portainerServices ? convertPortainerStackToDockerCompose(portainerServices) : null);
|
||||
</script>
|
||||
|
||||
|
||||
<svelte:head>
|
||||
{@html codeHighlighting}
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<h2>Installation</h2>
|
||||
|
||||
<h3>Via Portainer</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Ensure both
|
||||
<a href="https://docs.docker.com/engine/install/">Docker</a> and
|
||||
<a href="https://www.portainer.io/installation/">Portainer</a> are installed, and up-to-date
|
||||
</li>
|
||||
<li>Log into your Portainer web UI
|
||||
<li>Under Settings → App Templates, paste the below URL</li>
|
||||
<li>Head to Home → App Templates, and the list of apps will show up</li>
|
||||
<li>Select the app you wish to deploy, fill in any config options, and hit Deploy</li>
|
||||
</ol>
|
||||
|
||||
<h4>Template Import URL</h4>
|
||||
<pre class="template-url">{templatesUrl}</pre>
|
||||
<button on:click={() => copyToClipboard(templatesUrl)}>Copy</button>
|
||||
|
||||
<details>
|
||||
<summary>Show Me</summary>
|
||||
<img class="demo" src="https://i.ibb.co/XxGRjrs/portainer-templates-installation.gif" alt="demo" />
|
||||
</details>
|
||||
|
||||
{#if dockerRunCommand}
|
||||
<hr />
|
||||
<h3>Via Docker Run</h3>
|
||||
<div class="docker-run-command">
|
||||
<button class="docker-command-copy" on:click={() => copyToClipboard(dockerRunCommand)}>Copy</button>
|
||||
<Highlight language={shellHighlight} code={dockerRunCommand} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if dockerRunCommands && dockerRunCommands.length > 0}
|
||||
<hr />
|
||||
<h3>Via Docker Run</h3>
|
||||
{#each dockerRunCommands as command, index}
|
||||
<h4>Service #{index + 1} - {portainerServices[index].name}</h4>
|
||||
<div class="docker-run-command">
|
||||
<button class="docker-command-copy" on:click={() => copyToClipboard(command)}>Copy</button>
|
||||
<Highlight language={shellHighlight} code={command} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if dockerComposeFile}
|
||||
<hr />
|
||||
<h3>Via Docker Compose</h3>
|
||||
<p class="instructions">
|
||||
Save this file as <code>docker-compose.yml</code> and run <code>docker-compose up -d</code>
|
||||
<br>
|
||||
Use this only as a guide.
|
||||
</p>
|
||||
<div class="docker-compose-file">
|
||||
<button class="docker-command-copy" on:click={() => copyToClipboard(JSON.stringify(dockerComposeFile, null, 2))}>Copy</button>
|
||||
<Highlight language={yamlHighlight} code={dockerComposeFile} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr />
|
||||
<h3>Alternative Methods</h3>
|
||||
<p>For more installation options, see the <a href={gitHubRepo}>Documentation</a> in the GitHub repo</p>
|
||||
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
background: var(--card);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 1rem auto;
|
||||
max-width: 1000px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
h4 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
ol {
|
||||
margin: 0.5rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
li {
|
||||
counter-increment: item;
|
||||
}
|
||||
li:before {
|
||||
content: counter(item);
|
||||
color: var(--accent);
|
||||
margin-right: 0.5rem;
|
||||
font-weight: 600;
|
||||
width: 1ch;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
hr {
|
||||
opacity: 0.5;
|
||||
margin: 1.5rem auto;
|
||||
height: 2px;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
}
|
||||
pre {
|
||||
background: var(--card-2);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
width: fit-content;
|
||||
margin: 0.5rem 0;
|
||||
display: inline;
|
||||
border-radius: 6px;
|
||||
&.template-url {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
button {
|
||||
background: var(--background);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
font-family: Kanit;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--gradient);
|
||||
transform: scale(1.1) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
details {
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.demo {
|
||||
display: block;
|
||||
margin: 0.5rem auto;
|
||||
border-radius: 6px;
|
||||
max-width: 50rem;
|
||||
}
|
||||
.docker-run-command, .docker-compose-file {
|
||||
background: var(--card-2);
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
pre {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.docker-command-copy {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
.instructions {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
code {
|
||||
border-radius: 6px;
|
||||
padding: 0 0.25rem;
|
||||
background: var(--card-2);
|
||||
}
|
||||
}
|
||||
:global(.hljs) {
|
||||
background: var(--card-2);
|
||||
font-size: 1.1rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,60 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let searchTerm: string;
|
||||
export let isCategoriesVisible: boolean;
|
||||
export let toggleCategories: () => void;
|
||||
</script>
|
||||
|
||||
<div class="title-row">
|
||||
<h2>Template List</h2>
|
||||
<div class="filters">
|
||||
<button on:click={toggleCategories}>
|
||||
{isCategoriesVisible ? '▲' : '▼'} Categories
|
||||
</button>
|
||||
<input type="text" placeholder="Search..." bind:value={searchTerm} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: var(--max-width);
|
||||
flex-wrap: wrap;
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
.filters {
|
||||
input {
|
||||
background: var(--card);
|
||||
border: 1px solid transparent;
|
||||
color: var(--foreground);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
transition:all 0.3s ease-in-out;
|
||||
&:focus, &:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
color: var(--foreground);
|
||||
border: 1px solid transparent;
|
||||
padding: 0 0.3rem;
|
||||
margin: 0.25rem;
|
||||
line-height: 2rem;
|
||||
border-radius: 6px;
|
||||
text-transform: capitalize;
|
||||
background: var(--card);
|
||||
transition: all 0.3s ease-in-out;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
&:hover, &.selected {
|
||||
background: var(--gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,85 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import snarkdown from 'snarkdown';
|
||||
export let content: string | null = null;
|
||||
export let multiContent: { name: string, content: string, description: string, visible: false }[] | null = null;
|
||||
|
||||
let showDocs = false;
|
||||
|
||||
const toggleDocs = () => {
|
||||
showDocs = !showDocs;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<section class="docker-docs">
|
||||
<h2>Container Documentation</h2>
|
||||
{#if content}
|
||||
<button on:click={toggleDocs}>{ showDocs ? 'Hide' : 'Expand' } Content</button>
|
||||
{#if showDocs}
|
||||
<p transition:slide>{@html snarkdown(content)}</p>
|
||||
{/if}
|
||||
|
||||
{:else if multiContent && multiContent.length > 0}
|
||||
{#each multiContent as { name, description, content, visible }}
|
||||
<h3>{name} Documentation</h3>
|
||||
<p class="desc">{description || ''}</p>
|
||||
<button on:click={() => visible = !visible}>{ visible ? 'Hide' : 'Expand' } {name}</button>
|
||||
{#if visible}
|
||||
<p transition:slide>{@html snarkdown(content)}</p>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
.docker-docs {
|
||||
background: var(--card);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 1rem auto;
|
||||
max-width: 1000px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
button {
|
||||
background: var(--background);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
font-family: Kanit;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--gradient);
|
||||
transform: scale(1.1) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0.5rem 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.desc {
|
||||
opacity: 0.7;
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
:global(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
:global(a) {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
:global(pre) {
|
||||
background: var(--card-2);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||
<div class="nout">
|
||||
<h3>No Results 😢</h3>
|
||||
<p>
|
||||
<i>There weren't any templates found that matched the currently applied filters.</i><br><br>
|
||||
Check the raw <a href="https://github.com/Lissy93/portainer-templates/blob/main/templates.json"><code>templates.json</code></a> file
|
||||
to see all results.
|
||||
If you still can't find what you're looking for, why not
|
||||
<a href="https://github.com/Lissy93/portainer-templates#editing">submit a template</a>?
|
||||
Feel free to raise a ticket if you need support.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.nout {
|
||||
background: var(--card);
|
||||
border-radius: 6px;
|
||||
min-height: 8rem;
|
||||
margin: 1rem auto 5rem auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: 650px;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
code, a {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,56 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let searchTerm: string;
|
||||
export let selectedCategories: string[];
|
||||
export let clearSearch: () => void;
|
||||
export let numResults: number;
|
||||
export let totalResults: number;
|
||||
</script>
|
||||
|
||||
<div class="search-summary">
|
||||
{#if searchTerm}
|
||||
<p>
|
||||
Showing {numResults} of {totalResults}
|
||||
results, matching "<i>{searchTerm}</i>"
|
||||
{selectedCategories.length ? `in categories: ${selectedCategories.join(', ')}` : ''}
|
||||
</p>
|
||||
{:else if selectedCategories.length}
|
||||
<p>
|
||||
Showing {numResults} of {totalResults}
|
||||
results, matching categories: {selectedCategories.join(', ')}
|
||||
</p>
|
||||
{:else}
|
||||
<p>Click an app to view info, stats and usage docs</p>
|
||||
{/if}
|
||||
{#if searchTerm || selectedCategories.length}
|
||||
<button on:click={clearSearch}>⨯ Clear Filters</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.search-summary {
|
||||
margin: 0 1rem;
|
||||
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
p {
|
||||
opacity: 0.75;
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
background: var(--gradient);
|
||||
outline: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
color: var(--foreground);
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,113 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { TemplateOrService } from '$src/Types';
|
||||
|
||||
export let template: TemplateOrService;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="stats">
|
||||
{#if template.type}
|
||||
<span class="lbl">Type</span>
|
||||
{#if template.type === 1}
|
||||
<span class="val">Container</span>
|
||||
{:else if template.type === 2}
|
||||
<span class="val">Swarm</span>
|
||||
{:else if template.type === 3}
|
||||
<span class="val">Kubernetes</span>
|
||||
{:else}
|
||||
<span class="val">Unknown</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if template.platform}
|
||||
<span class="lbl">Platform</span>
|
||||
<code class="val">{template.platform}</code>
|
||||
{/if}
|
||||
{#if template.image}
|
||||
<span class="lbl">Image</span>
|
||||
<code class="val">{template.image}</code>
|
||||
{/if}
|
||||
{#if template.command}
|
||||
<span class="lbl">Command</span>
|
||||
<code class="val">{template.command}</code>
|
||||
{/if}
|
||||
{#if typeof template.interactive === 'boolean'}
|
||||
<span class="lbl">Interactive</span>
|
||||
<code class="val">{template.interactive ? 'Yes' : 'No'}</code>
|
||||
{/if}
|
||||
{#if template.ports}
|
||||
<span class="lbl">Ports</span>
|
||||
<p class="val">
|
||||
{#each template.ports as port}<code>{port}</code>{/each}
|
||||
</p>
|
||||
{/if}
|
||||
{#if template.volumes}
|
||||
<span class="lbl">Volumes</span>
|
||||
<p class="val">
|
||||
{#each template.volumes as volume}
|
||||
<code>
|
||||
{volume.container || volume}{volume?.bind? ' : ' + volume.bind : ''}
|
||||
</code>{/each}
|
||||
</p>
|
||||
{/if}
|
||||
{#if template.restart_policy}
|
||||
<span class="lbl">Restart Policy</span>
|
||||
<code class="val">{template.restart_policy}</code>
|
||||
{/if}
|
||||
{#if template.repository}
|
||||
<span class="lbl">Sourced</span>
|
||||
<a class="val" href={template.repository.url}>Repo</a>
|
||||
{/if}
|
||||
{#if template.entrypoint}
|
||||
<span class="lbl">Entrypoint</span>
|
||||
<code class="val">{template.entrypoint}</code>
|
||||
{/if}
|
||||
{#if template.build}
|
||||
<span class="lbl">Build</span>
|
||||
<code class="val">{template.build}</code>
|
||||
{/if}
|
||||
{#if template.env}
|
||||
<span class="lbl">Env Vars</span>
|
||||
<p class="val">
|
||||
{#each template.env as env}<code>{env.name}={env.set || env.value || env.default || '\'\''}</code>{/each}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.stats {
|
||||
min-width: 15rem;
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
place-items: baseline;
|
||||
background: var(--card-2);
|
||||
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.val {
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
white-space:nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span {
|
||||
font-style: italic;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,81 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Template } from '$src/Types';
|
||||
export let templates: Template[];
|
||||
import { lazyLoad } from '$lib/lazy-load';
|
||||
|
||||
const slugify = (title: string) => {
|
||||
return `/${title.toLowerCase().replace(/[^a-zA-Z ]/g, "").replaceAll(' ', '-')}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="templates">
|
||||
{#each templates as template (template.title)}
|
||||
<a class="template-card" href={slugify(template.title)}>
|
||||
<h3>{template.title}</h3>
|
||||
<div class="template-summary">
|
||||
<div class="left">
|
||||
<img class="loading" use:lazyLoad={template.logo} alt={template.title} />
|
||||
</div>
|
||||
<div class="txt">
|
||||
<p class="description" title={template.description}>{template.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section.templates {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: var(--max-width);
|
||||
.template-card {
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
background: var(--card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
transition:all 0.3s ease-in-out;
|
||||
max-width: 28rem;
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.template-summary {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
p, h3 {
|
||||
margin: 0;
|
||||
}
|
||||
img {
|
||||
width: 64px;
|
||||
max-height: 64px;
|
||||
border-radius: 6px;
|
||||
&.loading {
|
||||
padding: 0.2rem;
|
||||
background: var(--card-2);
|
||||
border-radius: 6px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
line-clamp: 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,60 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { gitHubRepo } from '$src/constants';
|
||||
export let templateName: string;
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>Template not Found 😢</h2>
|
||||
<p class="subtitle">It doesn't look like there was a templated named "<i>{templateName}</i>"</p>
|
||||
<p>
|
||||
You can try <a href="/">searching for another</a>, or if you think there's a mistake somewhere,
|
||||
please open an issue on the <a href={gitHubRepo} target="_blank">Github Repo</a>.
|
||||
</p>
|
||||
<a class="back-home" href="/">Back Home</a>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
background: var(--card);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 1rem auto;
|
||||
max-width: 1000px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
p {
|
||||
margin: 1rem auto;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
max-width: 40rem;
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
.back-home {
|
||||
background: var(--background);
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: var(--foreground);
|
||||
font-family: Kanit;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
background: var(--gradient);
|
||||
transform: scale(1.1) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -1,34 +0,0 @@
|
|||
// See how the options work here: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||
let options = {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 0
|
||||
}
|
||||
|
||||
export const lazyLoad = (image: any, src: string) => {
|
||||
const loaded = () => {
|
||||
image.classList.remove("loading");
|
||||
image.style.opacity = "1";
|
||||
};
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
image.src = src;
|
||||
if (image.complete) {
|
||||
loaded();
|
||||
} else {
|
||||
image.addEventListener("load", loaded);
|
||||
}
|
||||
}
|
||||
},
|
||||
options
|
||||
);
|
||||
observer.observe(image);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
image.removeEventListener("load", loaded);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { page, navigating } from '$app/stores';
|
||||
import { tick } from 'svelte';
|
||||
import Header from '$lib/Header.svelte';
|
||||
import Footer from '$lib/Footer.svelte';
|
||||
|
||||
let bottom = false;
|
||||
let showNav = false;
|
||||
|
||||
const scrollVisible = (): boolean => {
|
||||
return browser ?
|
||||
document.documentElement.clientHeight >= document.documentElement.scrollHeight
|
||||
: false;
|
||||
};
|
||||
|
||||
$: {
|
||||
updateFooter();
|
||||
if($navigating) updateFooter();
|
||||
showNav = !['/', '/index'].includes($page.url.pathname)
|
||||
}
|
||||
|
||||
async function updateFooter() {
|
||||
await tick();
|
||||
bottom = scrollVisible();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Portainer Templates</title>
|
||||
<meta name="description" content="A community-driven library of 1-click self-hosted apps" />
|
||||
<meta property="og:title" content="Portainer Templates" />
|
||||
<meta property="og:description" content="A community-driven library of 1-click self-hosted apps" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{import.meta.env.VITE_PUBLIC_BASE_URL}/" />
|
||||
<meta property="og:image" content="{import.meta.env.VITE_PUBLIC_BASE_URL}/banner.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Portainer Templates" />
|
||||
<meta name="twitter:description" content="A community-driven library of 1-click self-hosted apps" />
|
||||
<meta name="twitter:image" content="{import.meta.env.VITE_PUBLIC_BASE_URL}/banner.png" />
|
||||
<link rel="canonical" href="{import.meta.env.VITE_PUBLIC_BASE_URL}" />
|
||||
<meta name="theme-color" content="#0ba5ec" />
|
||||
</svelte:head>
|
||||
|
||||
{#if showNav}
|
||||
<Header />
|
||||
{/if}
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
<Footer {bottom} />
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import url('https://fonts.googleapis.com/css2?family=Kanit:wght@200;400;800&display=swap');
|
||||
:global(body) {
|
||||
--background: #101828;
|
||||
--foreground: #ffffff;
|
||||
--accent: #0ba5ec;
|
||||
--card: #1d2939;
|
||||
--card-2: #192432;
|
||||
--shadow: 1px 1px 3px 3px #0B9AEC8F;
|
||||
--gradient: linear-gradient(to right,#0B9AEC 0%,#6EDFDE 100%);
|
||||
--max-width: 1800px;
|
||||
margin: 0;
|
||||
font-family: 'Kanit', sans-serif;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
:global(::selection) {
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,32 +0,0 @@
|
|||
import { templates } from '$src/store';
|
||||
import { templatesUrl } from '$src/constants';
|
||||
|
||||
const makeCategories = (templates) => {
|
||||
// Get categories from templates
|
||||
const categories = templates.reduce((acc, { categories: templateCategories }) => {
|
||||
(templateCategories || []).forEach((category) => {
|
||||
acc[category] = (acc[category] || 0) + 1;
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Sort categories by count, and remove categories with only 1 template
|
||||
const sortedCategories = Object.fromEntries(
|
||||
Object.entries(categories)
|
||||
.filter(([, value]) => value > 3)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
);
|
||||
|
||||
return sortedCategories;
|
||||
};
|
||||
|
||||
|
||||
export const load = async () => {
|
||||
const data = await fetch(templatesUrl).then((res) => res.json());
|
||||
templates.set(data.templates);
|
||||
|
||||
return {
|
||||
templates: data.templates,
|
||||
categories: makeCategories(data.templates),
|
||||
}
|
||||
};
|
|
@ -1,97 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
|
||||
import Hero from '$lib/Hero.svelte';
|
||||
import ListFilter from '$lib/ListFilter.svelte';
|
||||
import Categories from '$lib/Categories.svelte';
|
||||
import SearchSummary from '$lib/SearchSummary.svelte';
|
||||
import Templates from '$lib/TemplateList.svelte';
|
||||
import NoResults from '$lib/NoResults.svelte';
|
||||
import Footer from '$lib/Footer.svelte';
|
||||
import type { Template } from '$src/Types';
|
||||
|
||||
export let data;
|
||||
|
||||
|
||||
|
||||
const preSelectedCategories = $page.url.searchParams.get('categories');
|
||||
|
||||
let searchTerm = '';
|
||||
|
||||
let selectedCategories: string[] = preSelectedCategories?.split(',') || [];
|
||||
|
||||
let showCategories = !!preSelectedCategories || false;
|
||||
|
||||
$: filteredTemplates = data.templates.filter((template: Template) => {
|
||||
const compareStr = (str1: string, str2: string) =>
|
||||
(str1 || '').toLowerCase().includes(str2.toLowerCase());
|
||||
|
||||
if (selectedCategories.length) {
|
||||
const templateCategories = (template.categories || []).map((c) => c.toLowerCase());
|
||||
const hasSelectedCategory = selectedCategories.some((cat) =>
|
||||
templateCategories.includes(cat.toLocaleLowerCase())
|
||||
);
|
||||
if (!hasSelectedCategory) return false;
|
||||
}
|
||||
return (
|
||||
compareStr(template.title, searchTerm) ||
|
||||
compareStr(template.description, searchTerm) ||
|
||||
compareStr((template.categories || []).join(''), searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
const showHideCategoryList = () => {
|
||||
showCategories = !showCategories;
|
||||
};
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
if (selectedCategories.includes(category)) {
|
||||
selectedCategories = selectedCategories.filter((cat) => cat !== category);
|
||||
} else {
|
||||
selectedCategories = [...selectedCategories, category];
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
searchTerm = '';
|
||||
selectedCategories = [];
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Main title, and CTA buttons -->
|
||||
<Hero />
|
||||
|
||||
<!-- Search bar, and Templates sub-title -->
|
||||
<ListFilter
|
||||
bind:searchTerm={searchTerm}
|
||||
toggleCategories={showHideCategoryList}
|
||||
isCategoriesVisible={showCategories}
|
||||
/>
|
||||
|
||||
<!-- List of categories to filter by -->
|
||||
{#if showCategories}
|
||||
<Categories
|
||||
categories={data.categories}
|
||||
selectedCategories={selectedCategories}
|
||||
toggleCategory={toggleCategory}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Text showing num results, and users search term + filters -->
|
||||
<SearchSummary
|
||||
searchTerm={searchTerm}
|
||||
selectedCategories={selectedCategories}
|
||||
clearSearch={clearSearch}
|
||||
numResults={filteredTemplates.length}
|
||||
totalResults={data.templates.length}
|
||||
/>
|
||||
|
||||
<!-- List of available templates (filtered, if needed) -->
|
||||
<Templates templates={filteredTemplates} />
|
||||
|
||||
<!-- If there are no templates matching search term, show lil message -->
|
||||
{#if !filteredTemplates.length}
|
||||
<NoResults />
|
||||
{/if}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import yaml from 'js-yaml';
|
||||
|
||||
import { get } from 'svelte/store';
|
||||
import { templatesUrl } from '$src/constants';
|
||||
import { templates } from '$src/store';
|
||||
|
||||
/* Based on the current page name, find the corresponding template */
|
||||
const findTemplate = (templates: any, slug: string) => {
|
||||
return templates.find((temp: Template) =>
|
||||
temp.title.toLowerCase().replace(/[^a-zA-Z ]/g, "").replaceAll(' ', '-') === slug
|
||||
);
|
||||
};
|
||||
|
||||
/* With a given image name, fetch stats from DockerHub registry */
|
||||
const getDockerHubStats = async (image: string): Promise<DockerHubResponse | null> => {
|
||||
if (!image) return null;
|
||||
const [imageName, tag] = image.split(':');
|
||||
const [namespace, repo] = imageName.includes('/') ? imageName.split('/') : ['library', imageName];
|
||||
const apiEndpoint = `https://hub.docker.com/v2/repositories/${namespace}/${repo}/`;
|
||||
|
||||
return await fetch(apiEndpoint)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
const getServices = async (template): Promise<Service[]> => {
|
||||
try {
|
||||
if (template?.repository) {
|
||||
const { url: repoUrl, stackfile } = template.repository;
|
||||
const path = `${repoUrl.replace('github.com', 'raw.githubusercontent.com')}/HEAD/${stackfile}`;
|
||||
const response = await fetch(path);
|
||||
const data = await response.text();
|
||||
const parsedData = yaml.load(data);
|
||||
const someServices: Service[] = [];
|
||||
if (!parsedData.services) return [];
|
||||
|
||||
Object.keys(parsedData.services).forEach((service) => {
|
||||
const serviceData = parsedData.services[service];
|
||||
someServices.push({
|
||||
name: service,
|
||||
image: serviceData.image,
|
||||
entrypoint: serviceData.entrypoint,
|
||||
command: serviceData.command,
|
||||
ports: serviceData.ports,
|
||||
build: serviceData.build,
|
||||
interactive: serviceData.interactive,
|
||||
volumes: serviceData.volumes?.map((vol) => ({
|
||||
bind: vol.split(':')[0],
|
||||
container: vol.split(':')[1],
|
||||
})),
|
||||
restart_policy: serviceData.restart,
|
||||
env: Object.keys(serviceData.environment || {}).map((envName) => {
|
||||
if (typeof envName === 'string') {
|
||||
const nowItsArray = serviceData.environment[envName].split('=') || [];
|
||||
return { name: nowItsArray[0] || '', value: nowItsArray[1] || '' }
|
||||
}
|
||||
return { name: envName, value: serviceData.environment[envName] }
|
||||
}),
|
||||
});
|
||||
});
|
||||
return someServices;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching or parsing YAML:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/* Format results for returning to component */
|
||||
const returnResults = async (templates, templateSlug) => {
|
||||
// Find template, based on slug
|
||||
let template = findTemplate(get(templates), templateSlug);
|
||||
|
||||
// Fetch service info from associated stackfile, if it exists
|
||||
let services = template?.repository ? await getServices(template) : [];
|
||||
|
||||
// If only 1 service, merge it with the template
|
||||
if (services.length === 1) {
|
||||
template = {...template, ...services[0]};
|
||||
} else if (services.length > 1) {
|
||||
// If made up from multiple services, fetch Docker info for each image
|
||||
services = await Promise.all(
|
||||
services.map(async (service) => {
|
||||
const dockerStats = await getDockerHubStats(service.image);
|
||||
return { ...service, dockerStats };
|
||||
})
|
||||
);
|
||||
}
|
||||
// If image specified, fetch Docker image info from DockerHub
|
||||
const dockerStats = template?.image ? await getDockerHubStats(template.image) : null;
|
||||
return { template, dockerStats, services }
|
||||
};
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const templateSlug = params.slug as string;
|
||||
if (get(templates) && get(templates).length > 0) {
|
||||
return returnResults(templates, templateSlug);
|
||||
} else {
|
||||
const data = await fetch(templatesUrl).then((res) => res.json());
|
||||
templates.set(data.templates);
|
||||
return returnResults(templates, templateSlug);
|
||||
}
|
||||
};
|
|
@ -1,202 +0,0 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import ServiceStats from '$lib/ServiceStats.svelte';
|
||||
import TemplateNotFound from '$lib/TemplateNotFound.svelte';
|
||||
import DockerStats from '$lib/DockerStats.svelte';
|
||||
import MdContent from '$lib/MdContent.svelte';
|
||||
import InstallationInstructions from '$lib/InstallationInstructions.svelte';
|
||||
|
||||
import type { Template, Service, DockerHubResponse } from '$src/Types';
|
||||
|
||||
const urlSlug = $page.params.slug;
|
||||
const template = $page.data.template as Template;
|
||||
const dockerStats = $page.data.dockerStats as DockerHubResponse;
|
||||
const services = $page.data.services as Service[];
|
||||
|
||||
const makeMultiDoc = (services: Service[]) => {
|
||||
return services.map((s) => {
|
||||
return s?.dockerStats?.full_description ? {
|
||||
name: s.name,
|
||||
description: s.dockerStats.description,
|
||||
content: s.dockerStats.full_description,
|
||||
visible: false,
|
||||
} : null;
|
||||
}).filter((thingy) => thingy !== null);
|
||||
};
|
||||
|
||||
const makeMetaDescription = () => {
|
||||
return `Installation guide for ${template.title}, using Portainer, Docker Run or Docker-Compose. `
|
||||
+`Portainer-Templates is a community driven repository of Portainer Templates for Self-Hosted apps. \n`
|
||||
+`${template.description}`;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{template.title} | Portainer Templates</title>
|
||||
<meta name="description" content={makeMetaDescription()} />
|
||||
<meta property="og:title" content="{template.title} | Portainer Templates" />
|
||||
<meta property="og:description" content={makeMetaDescription()} />
|
||||
<meta property="og:url" content="{import.meta.env.VITE_PUBLIC_BASE_URL}/{urlSlug}" />
|
||||
<meta name="twitter:title" content="{template.title} | Portainer Templates" />
|
||||
<meta name="twitter:description" content={makeMetaDescription()} />
|
||||
<link rel="canonical" href="{import.meta.env.VITE_PUBLIC_BASE_URL}/{urlSlug}" />
|
||||
</svelte:head>
|
||||
|
||||
{#if template}
|
||||
<section class="summary-section">
|
||||
<h1>
|
||||
{#if template.logo} <img src={template.logo} /> {/if}
|
||||
{template.title}
|
||||
</h1>
|
||||
{#if template.categories || template.category }
|
||||
<p class="tags">
|
||||
{#each (template.categories || template.category || []) as tag}
|
||||
<a href="/?categories={tag}"><span>{tag}</span></a>
|
||||
{/each}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="content">
|
||||
<div class="left">
|
||||
<p class="description">{template.description}</p>
|
||||
{#await template then returnedTemplate}
|
||||
{#if dockerStats && dockerStats.name}
|
||||
<DockerStats info={dockerStats} />
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<ServiceStats template={template} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#await services then returnedServices}
|
||||
{#if returnedServices && returnedServices.length > 1}
|
||||
<section class="service-section">
|
||||
<h2>Services</h2>
|
||||
<div class="service-list">
|
||||
{#each returnedServices as service}
|
||||
<div class="service-each">
|
||||
<h3>{service.name}</h3>
|
||||
<div class="service-data">
|
||||
<ServiceStats template={service} />
|
||||
{#if service.dockerStats && service.dockerStats.name}
|
||||
<DockerStats info={service.dockerStats} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
<InstallationInstructions portainerTemplate={template} portainerServices={services || null} />
|
||||
|
||||
{#if dockerStats?.full_description}
|
||||
<MdContent content={dockerStats.full_description} />
|
||||
{:else if services.length > 0}
|
||||
<MdContent multiContent={makeMultiDoc(services)} />
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<TemplateNotFound templateName={urlSlug} />
|
||||
{/if}
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
max-width: 1000px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
.summary-section {
|
||||
background: var(--card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
img {
|
||||
border-radius: 6px;
|
||||
width: 64px;
|
||||
max-height: 64px;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
gap: 0.5rem;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
span {
|
||||
&:before {
|
||||
content: '#';
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:not(:last-child)::after {
|
||||
content: ',';
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
p.description {
|
||||
background: var(--card-2);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.service-section {
|
||||
background: var(--card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.service-list {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
h3 {
|
||||
margin: 0.5rem 0;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.service-each {
|
||||
.service-data {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,41 +0,0 @@
|
|||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
import { templatesUrl, baseUrl } from '$src/constants';
|
||||
import type { Template } from '$src/Types';
|
||||
|
||||
const fetchData = async () => {
|
||||
const data = await fetch(templatesUrl).then((res) => res.json());
|
||||
return await data.templates.map((d: Template) => `${baseUrl}/${d.title.toLowerCase().replace(/[^a-zA-Z ]/g, "").replaceAll(' ', '-')}`);
|
||||
};
|
||||
|
||||
const generationDate = () => {
|
||||
const date = new Date();
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const data = await fetchData();
|
||||
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>${baseUrl}</loc>
|
||||
<lastmod>${generationDate()}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1</priority>
|
||||
</url>
|
||||
${data.map((url: string) => `
|
||||
<url>
|
||||
<loc>${url}</loc>
|
||||
<lastmod>${generationDate()}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>`)
|
||||
.join('')}
|
||||
</urlset>`;
|
||||
|
||||
return new Response(sitemap, {
|
||||
headers: { 'Content-Type': 'application/xml' }
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<script lang="ts">
|
||||
import InstallationInstructions from '$lib/InstallationInstructions.svelte';
|
||||
</script>
|
||||
|
||||
<InstallationInstructions />
|
|
@ -1,3 +0,0 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
export const templates = writable([]);
|
|
@ -1,88 +0,0 @@
|
|||
|
||||
import yaml from 'js-yaml';
|
||||
import type { Template, Volume, Service, DockerCompose } from '$src/Types';
|
||||
|
||||
export const generateDockerRunCommand = (template: Template) => {
|
||||
let command = `docker run -d \\ \n`;
|
||||
if (template.ports) {
|
||||
template.ports.forEach((port) => {
|
||||
command += ` -p ${port} \\\n`;
|
||||
});
|
||||
}
|
||||
if (template.env) {
|
||||
template.env.forEach((env) => {
|
||||
command += ` -e ${env.name}=\${${env.name}} \\\n`;
|
||||
});
|
||||
}
|
||||
if (template.volumes) {
|
||||
template.volumes.forEach((volume: Volume) => {
|
||||
const readOnly = volume.readonly ? ":ro" : "";
|
||||
command += ` -v ${volume.bind}:${volume.container}${readOnly} \\\n`;
|
||||
});
|
||||
}
|
||||
if (template.restart_policy) {
|
||||
command += ` --restart=${template.restart_policy} \\\n`;
|
||||
}
|
||||
command += ` ${template.image}`;
|
||||
return command;
|
||||
};
|
||||
|
||||
export const generateDockerRunCommands = (stack: Service[]) => {
|
||||
const commands = stack.filter((s) => s.image).map((service) => {
|
||||
let cmd = `docker run --name ${service.name} -d \\\n`;
|
||||
if (service.command) {
|
||||
cmd += ` ${service.command} \\\n`;
|
||||
}
|
||||
if (service.env) {
|
||||
service.env.forEach((envVar) => {
|
||||
cmd += ` -e "${envVar.value}" \\\n`;
|
||||
});
|
||||
}
|
||||
if (service.ports) {
|
||||
service.ports.forEach((port) => {
|
||||
cmd += ` -p ${port} \\\n`;
|
||||
});
|
||||
}
|
||||
if (service.volumes) {
|
||||
service.volumes.forEach((volume) => {
|
||||
cmd += ` -v ${volume.bind}:${volume.container} \\\n`;
|
||||
});
|
||||
}
|
||||
if (service.restart_policy) {
|
||||
cmd += ` --restart=${service.restart_policy} \\\n`;
|
||||
}
|
||||
cmd += ` ${service.image}`;
|
||||
return cmd;
|
||||
});
|
||||
return commands;
|
||||
}
|
||||
|
||||
export const convertToDockerCompose = (template: Template) => {
|
||||
const serviceName = template.title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
||||
const dockerCompose: DockerCompose = {
|
||||
version: "3.8",
|
||||
services: { [serviceName]: { image: template.image } },
|
||||
};
|
||||
if (template.ports && template.ports.length > 0) {
|
||||
dockerCompose.services[serviceName].ports = template.ports.map((port) => port.replace('/', ':'));
|
||||
}
|
||||
if (template.env && template.env.length > 0) {
|
||||
dockerCompose.services[serviceName].environment = template.env.reduce((envVars, envVar) => {
|
||||
envVars[envVar.name] = envVar.set || "";
|
||||
return envVars;
|
||||
}, {});
|
||||
}
|
||||
if (template.volumes && template.volumes.length > 0) {
|
||||
dockerCompose.services[serviceName].volumes = template.volumes.map(
|
||||
(volume) => `${volume.bind || ""}:${volume.container}`
|
||||
);
|
||||
}
|
||||
|
||||
return yaml.dump(dockerCompose);
|
||||
};
|
||||
|
||||
export const convertPortainerStackToDockerCompose = (stack: Service[]) => {
|
||||
const composeStack = stack.map(({ dockerStats, ...s }) => s);
|
||||
return yaml.dump(composeStack);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 328 KiB |
Before Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "Portainer Templates",
|
||||
"short_name": "Portainer Templates",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#6EDFDE",
|
||||
"background_color": "#101828",
|
||||
"display": "standalone"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
'$src/*': 'src/*',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
20859
templates.json
Normal file
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { sentrySvelteKit } from '@sentry/sveltekit';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), sentrySvelteKit()],
|
||||
});
|