Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9190ba5a | ||
|
|
8688e0a3e0 | ||
|
|
464812562e | ||
|
|
7103b0d404 | ||
|
|
36246d81b1 | ||
|
|
714a528bce | ||
|
|
e90e984b72 | ||
|
|
ad09771f54 | ||
|
|
7d039bbff8 | ||
|
|
c3d7694419 | ||
|
|
9c95bc5e82 | ||
|
|
f39c382a91 | ||
|
|
391f483a05 | ||
|
|
774de9c902 | ||
|
|
025c8f4642 | ||
|
|
eb3e5e5ade | ||
|
|
112a8b62b3 | ||
|
|
2ae6293544 | ||
|
|
0f4d9e5c20 | ||
|
|
3ed36f61a6 | ||
|
|
281021fd12 | ||
|
|
e93451121d | ||
|
|
da37cae7b0 | ||
|
|
ba61f69f1a | ||
|
|
ab2b73cdd4 | ||
|
|
56b52ade1e | ||
|
|
11e43843a8 | ||
|
|
30418d0ffe | ||
|
|
cd6e3342cf | ||
|
|
93b9dcb68a | ||
|
|
983ac541ad | ||
|
|
12171606e9 | ||
|
|
fef32824de | ||
|
|
ce669212f0 | ||
|
|
7280e4b0ab | ||
|
|
75266a1915 | ||
|
|
9f21da5ad0 | ||
|
|
bf9a4585fe | ||
|
|
7bb4421d0d | ||
|
|
5c0c537aaa | ||
|
|
688ad6420e | ||
|
|
cb8fcf5df2 | ||
|
|
2fb9a402eb | ||
|
|
4d5dbf26c4 |
No files matched your search
@@ -1,122 +0,0 @@
|
|||||||
|
|
||||||
<h1 align="center">Portainer Templates</h1>
|
|
||||||
<p align="center"><i>A compiled list of 400+ ready to go Portainer App templates</i></p>
|
|
||||||
<p align="center">
|
|
||||||
<img width="200" src="https://i.ibb.co/hMymwH0/portainer-templates-small.png" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> **TL;DR** Under Settings → App Templates in your Portainer GUI, paste this URL:<br>
|
|
||||||
> `https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json`<br>
|
|
||||||
|
|
||||||
|
|
||||||
## Intro
|
|
||||||
|
|
||||||
In Portainer, [App Templates](https://docs.portainer.io/user/docker/templates) enable you to easily deploy a container with a predetermined configuration, while allowing you to customize options through the web UI. Both single containers, and stacks are supported. While Portainer ships with some default templates (see [portainer/templates](https://github.com/portainer/templates)), it's often helpful to have 1-click access to many more apps, without having to constantly switch template sources.
|
|
||||||
|
|
||||||
This repo combines app templates from several [sources](#sources), to create a ready-to-go template file containing all the apps you'll ever need.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Log into your Portainer web UI
|
|
||||||
2. Under Settings --> App Templates, update the URL to
|
|
||||||
- `https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json`
|
|
||||||
3. Now under Home --> App Templates, you should see all apps. Click one to deploy.
|
|
||||||
|
|
||||||
Alternatively, when you start Portainer, you can append the `--templates` flag pointing to the templates URL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Self-Hosting
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/lissy93/portainer-templates.git portainer-templates
|
|
||||||
cd portainer-templates
|
|
||||||
docker build -t portainer-templates .
|
|
||||||
docker run -d -p "8080:80" portainer-templates
|
|
||||||
```
|
|
||||||
|
|
||||||
Your templates file will then be served up, at: `http://docker-host:8080/templates.json`
|
|
||||||
|
|
||||||
Or, to mount the `templates.json` file to your container, so that you can make changes to it, and have them show up within Portainer
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -d -p "8080:80" -v "${PWD}/templates.json:/usr/share/nginx/html/templates.json" portainer-templates
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Editing
|
|
||||||
|
|
||||||
The `template.json` file is generated using the scripts in [`lib`](https://github.com/Lissy93/portainer-templates/tree/main/lib), using GitHub Actions.
|
|
||||||
Running the `make` command will download all listed sources, parse them, and combine them outputting the `templates.json` file.
|
|
||||||
|
|
||||||
### Adding a new Source
|
|
||||||
Just place a link to the source, along with your chosen name in the [`sources.csv`](https://github.com/Lissy93/portainer-templates/blob/main/sources.csv) file.
|
|
||||||
When the action runs, it will download the content, parse it and add it to the final template.
|
|
||||||
|
|
||||||
### Adding a Template / Template list
|
|
||||||
Alternatively, place your template file within the [`sources`](https://github.com/Lissy93/portainer-templates/tree/main/sources) directory, and it will be automatically combined into the main `template.json`.
|
|
||||||
Be sure that your template corresponds to [Portainer's App Template JSON Format](https://docs.portainer.io/advanced/app-templates/format).
|
|
||||||
|
|
||||||
### Validating Templates
|
|
||||||
There is a schema defined in [`Schema.json`](https://github.com/Lissy93/portainer-templates/blob/main/Schema.json), which can be used to validate any Portainer template.
|
|
||||||
Run `make validate` to ensure your template conforms to Portainer's App Template [specification](https://docs.portainer.io/advanced/app-templates/format).
|
|
||||||
|
|
||||||
### Maintaining your own Templates
|
|
||||||
If you'd prefer to maintain your own templates, while using the templates included here as a base, then fork the repository, and update `lissy93` with your GitHub username
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
The templates here are composed from the following sources. Full credit to the authors of each
|
|
||||||
|
|
||||||
- [dnburgess](https://github.com/dnburgess/self-hosted-template) <sup>[`template.json`](https://raw.githubusercontent.com/dnburgess/self-hosted-template/master/template.json)</sup>
|
|
||||||
- [qballjos](https://github.com/Qballjos/portainer_templates) <sup>[`template.json`](https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Template/template.json)</sup>
|
|
||||||
- [SelfhostedPro](https://github.com/SelfhostedPro/selfhosted_templates) <sup>[`template.json`](https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/portainer-2.0/Template/template.json)</sup>
|
|
||||||
- [technorabilia](https://github.com/technorabilia/portainer-templates) <sup>[`template.json`](https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json)</sup>
|
|
||||||
- [mikestraney](https://github.com/mikestraney/portainer-templates) <sup>[`template.json`](https://raw.githubusercontent.com/mikestraney/portainer-templates/master/templates.json)</sup>
|
|
||||||
- [xneo1](https://github.com/xneo1/portainer_templates) <sup>[`template.json`](https://raw.githubusercontent.com/xneo1/portainer_templates/master/Template/template.json)</sup>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Apps and Stacks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- License + Copyright -->
|
|
||||||
<p align="center">
|
|
||||||
<i>© <a href="https://aliciasykes.com">Alicia Sykes</a> 2023</i><br>
|
|
||||||
<i>Licensed under <a href="https://gist.github.com/Lissy93/143d2ee01ccc5c052a17">MIT</a></i><br>
|
|
||||||
<a href="https://github.com/lissy93"><img src="https://i.ibb.co/4KtpYxb/octocat-clean-mini.png" /></a><br>
|
|
||||||
<sup>Thanks for visiting :)</sup>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Dinosaur -->
|
|
||||||
<!--
|
|
||||||
. - ~ ~ ~ - .
|
|
||||||
.. _ .-~ ~-.
|
|
||||||
//| \ `..~ `.
|
|
||||||
|| | } } / \ \
|
|
||||||
(\ \\ \~^..' | } \
|
|
||||||
\`.-~ o / } | / \
|
|
||||||
(__ | / | / `.
|
|
||||||
`- - ~ ~ -._| /_ - ~ ~ ^| /- _ `.
|
|
||||||
| / | / ~-. ~- _
|
|
||||||
|_____| |_____| ~ - . _ _~_-_
|
|
||||||
-->
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
name: 🏗️ Build + Publish templates.json file
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
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..."
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
git commit -m "Updates templates (auto-generated, on ${{ steps.date.outputs.date }})"
|
|
||||||
git push
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
@@ -1,5 +1,55 @@
|
|||||||
FROM nginx:stable-alpine
|
FROM node:18-alpine AS BUILD_IMAGE
|
||||||
|
|
||||||
COPY templates.json /usr/share/nginx/html/templates.json
|
# Set the platform to build image for
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||||
|
|
||||||
EXPOSE 80
|
# 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
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.PHONY: all install_requirements download combine
|
|
||||||
|
|
||||||
all: install_requirements download combine
|
|
||||||
|
|
||||||
install_requirements:
|
|
||||||
pip install -r lib/requirements.txt
|
|
||||||
|
|
||||||
download:
|
|
||||||
python lib/download.py
|
|
||||||
|
|
||||||
combine:
|
|
||||||
python lib/combine.py
|
|
||||||
|
|
||||||
validate:
|
|
||||||
python lib/validate.py
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
____ _ _
|
||||||
|
| _ \ ___ _ __| |_ __ _(_)_ __ ___ _ __
|
||||||
|
| |_) / _ \| '__| __/ _` | | '_ \ / _ \ '__|
|
||||||
|
| __/ (_) | | | || (_| | | | | | __/ |
|
||||||
|
|_|___\___/|_| \__\__,_|_|_| |_|\___|_|
|
||||||
|
|_ _|__ _ __ ___ _ __ | | __ _| |_ ___ ___
|
||||||
|
| |/ _ \ '_ ` _ \| '_ \| |/ _` | __/ _ \/ __|
|
||||||
|
| | __/ | | | | | |_) | | (_| | || __/\__ \
|
||||||
|
|_|\___|_| |_| |_| .__/|_|\__,_|\__\___||___/
|
||||||
|
|_|
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
# Open the file
|
|
||||||
with open(templates_src_dir + file) as f:
|
|
||||||
if file.endswith('.json'):
|
|
||||||
# Load the JSON into a variable
|
|
||||||
data = json.load(f)['templates']
|
|
||||||
# Append the template object to the templates list
|
|
||||||
templates = templates + data
|
|
||||||
|
|
||||||
# Remove duplicates
|
|
||||||
seen_titles = set()
|
|
||||||
filtered_data = [x for x in templates if x['title'] not in seen_titles and not seen_titles.add(x['title'])]
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import os
|
|
||||||
import csv
|
|
||||||
import requests
|
|
||||||
|
|
||||||
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):
|
|
||||||
file_path = os.path.join(destination_dir, filename)
|
|
||||||
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())
|
|
||||||
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:#
|
|
||||||
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')
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
requests
|
|
||||||
jsonschema
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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)
|
|
||||||
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()
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
dnburgess_templates, https://raw.githubusercontent.com/dnburgess/self-hosted-template/master/template.json
|
|
||||||
qballjos_templates, https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Template/template.json
|
|
||||||
selfhostedpro_templates, https://raw.githubusercontent.com/SelfhostedPro/selfhosted_templates/portainer-2.0/Template/template.json
|
|
||||||
technorabilia_templates, https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json
|
|
||||||
mikestraney_templates, https://raw.githubusercontent.com/mikestraney/portainer-templates/master/templates.json
|
|
||||||
xneo1_templates, https://raw.githubusercontent.com/xneo1/portainer_templates/master/Template/template.json
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2",
|
|
||||||
"templates": []
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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]: {} };
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// 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 {};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
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';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let name: string; // The name of the icon to display
|
||||||
|
export let width = '16px'; // The width of the icon
|
||||||
|
export let height = '16px'; // The height of the icon
|
||||||
|
export let color = 'currentcolor'; // The color of the icon
|
||||||
|
export let hoverColor = 'currentColor'; // The color of the icon on hover
|
||||||
|
export let focusable = 'false'; // Whether the icon is focusable
|
||||||
|
export let hover = false; // Manually set hover state
|
||||||
|
|
||||||
|
const availableIcons = [
|
||||||
|
'github',
|
||||||
|
'portainer',
|
||||||
|
'stack',
|
||||||
|
'container',
|
||||||
|
'swarm',
|
||||||
|
'linux',
|
||||||
|
'windows',
|
||||||
|
'unknown',
|
||||||
|
'user',
|
||||||
|
'status',
|
||||||
|
'updated',
|
||||||
|
'published',
|
||||||
|
'star',
|
||||||
|
'download',
|
||||||
|
'whale'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface Icon {
|
||||||
|
box: number;
|
||||||
|
name: typeof availableIcons[number];
|
||||||
|
svg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icons: Icon[] = [
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'github',
|
||||||
|
svg: '<path d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 20,
|
||||||
|
name: 'portainer',
|
||||||
|
svg: '<path d="M12.504 0v1.023l-.01-.015-6.106 3.526H3.417v.751h5.359v3.638h1.942V5.284h1.786v10.416c.027 0 .54-.01.751.091V5.285h.531v10.608c.293.147.55.312.751.54V5.286h6.046v-.75h-1.267l-6.061-3.5V0zm0 1.87v2.664H7.889zm.751.031l4.56 2.633h-4.56zM9.142 5.285h1.21v1.686h-1.21zm-4.736 2.73v1.951h1.942v-1.95zm2.19 0v1.951h1.941v-1.95zm-2.19 2.171v1.951h1.942v-1.95zm2.19 0v1.951h1.941v-1.95zm2.18 0v1.951h1.942v-1.95zM4.36 12.43a3.73 3.73 0 00-.494 1.851c0 1.227.604 2.308 1.52 2.986.239-.064.477-.1.724-.11.1 0 .165.01.266.019.284-1.191 1.383-1.988 2.665-1.988.724 0 1.438.201 1.924.668.229-.476.302-1.007.302-1.575 0-.65-.165-1.292-.494-1.85zm4.828 3.16c-1.21 0-2.226.844-2.492 1.97a.922.922 0 00-.275-.009 2.559 2.559 0 00-2.564 2.556 2.565 2.565 0 003.096 2.5A2.579 2.579 0 009.233 24c.862 0 1.622-.43 2.09-1.081a2.557 2.557 0 004.186-1.97c0-.567-.193-1.099-.504-1.52a2.557 2.557 0 00-3.866-2.94 2.574 2.574 0 00-1.951-.898z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'stack',
|
||||||
|
svg: '<path d="M264.5 5.2c14.9-6.9 32.1-6.9 47 0l218.6 101c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 149.8C37.4 145.8 32 137.3 32 128s5.4-17.9 13.9-21.8L264.5 5.2zM476.9 209.6l53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 277.8C37.4 273.8 32 265.3 32 256s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0l152-70.2zm-152 198.2l152-70.2 53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 405.8C37.4 401.8 32 393.3 32 384s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'container',
|
||||||
|
svg: '<path d="M32 32C14.3 32 0 46.3 0 64S14.3 96 32 96V416c-17.7 0-32 14.3-32 32s14.3 32 32 32H608c17.7 0 32-14.3 32-32s-14.3-32-32-32V96c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM176 152V360c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24s24 10.7 24 24zm112 0V360c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24s24 10.7 24 24zm112 0V360c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24s24 10.7 24 24zm112 0V360c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24s24 10.7 24 24z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'swarm',
|
||||||
|
svg: '<path d="M240 80v32h32V80H240zM224 32h64c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H224c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32zM136.5 268.2l27.7-16-16-27.7-27.7 16 16 27.7zM71 250.7c-8.8-15.3-3.6-34.9 11.7-43.7l55.4-32c15.3-8.8 34.9-3.6 43.7 11.7l32 55.4c8.8 15.3 3.6 34.9-11.7 43.7l-55.4 32c-15.3 8.8-34.9 3.6-43.7-11.7L71 250.7zM304 272h32V240H304v32zm-48-48c0-17.7 14.3-32 32-32h64c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H288c-17.7 0-32-14.3-32-32V224zM368 432h32V400H368v32zm-48-48c0-17.7 14.3-32 32-32h64c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H352c-17.7 0-32-14.3-32-32V384zM208 400v32h32V400H208zm-16-48h64c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32V384c0-17.7 14.3-32 32-32zM48 432H80V400H48v32zM0 384c0-17.7 14.3-32 32-32H96c17.7 0 32 14.3 32 32v64c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V384z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 25,
|
||||||
|
name: 'linux',
|
||||||
|
svg: '<path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.Line truncated
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 25,
|
||||||
|
name: 'windows',
|
||||||
|
svg: '<path d="M0,0H11.377V11.372H0ZM12.623,0H24V11.372H12.623ZM0,12.623H11.377V24H0Zm12.623,0H24V24H12.623"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'unknown',
|
||||||
|
svg: '<path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm73.8 133.3c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L248 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H190.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM192 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'user',
|
||||||
|
svg: '<path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'status',
|
||||||
|
svg: '<path d="M269.4 2.9C265.2 1 260.7 0 256 0s-9.2 1-13.4 2.9L54.3 82.8c-22 9.3-38.4 31-38.3 57.2c.5 99.2 41.3 280.7 213.6 363.2c16.7 8 36.1 8 52.8 0C454.7 420.7 495.5 239.2 496 140c.1-26.2-16.3-47.9-38.3-57.2L269.4 2.9zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'updated',
|
||||||
|
svg: '<path d="M67.2 83.2L0 16V192H176l-63.5-63.5C147.7 88.9 198.9 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4l-36.6 52.5C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C181.2 0 113.9 32.1 67.2 83.2zM280 128H232v24V256v9.9l7 7 64 64 17 17L353.9 320l-17-17-57-57V152 128z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'published',
|
||||||
|
svg: '<path d="M96 32V64H48C21.5 64 0 85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 48-48V192z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 560,
|
||||||
|
name: 'star',
|
||||||
|
svg: '<path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 500,
|
||||||
|
name: 'download',
|
||||||
|
svg: '<path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
box: 640,
|
||||||
|
name: 'whale',
|
||||||
|
svg: '<path d="M175.8 13.4c-1-5.9-5.2-10.8-10.9-12.6s-12-.3-16.2 3.9L108 45.4C89.7 63.7 81.4 89.6 85.6 115.1l7.9 47.4L43.6 212.4C15.7 240.3 0 278.2 0 317.6C0 407.3 72.7 480 162.4 480H544c53 0 96-43 96-96V272.1C640 192.5 575.5 128 495.9 128c-82.5 0-162.4 29.5-225 83.2L156.4 309.4c-8 6.8-18.1 10.6-28.6 10.6h-1.1c-18.3 0-34.5-11.7-40.3-29c-4-12-2.4-25.2 4.3-35.9l37.6-60.1 44.7 7.5c25.5 4.2 51.5-4.1 69.7-22.3l40.7-40.7c4.2-4.2 5.8-10.5 3.9-16.2s-6.7-9.9-12.6-10.9L189.9 98.1 175.8 13.4zM400 280a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let displayIcon = icons.find((e) => e.name === name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{focusable}
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
class={$$props.class + `${hover ? ' hover' : ''}`}
|
||||||
|
style={`--iconColor: ${color}; --iconHoverColor: ${hoverColor};`}
|
||||||
|
viewBox="0 0 {displayIcon?.box} {displayIcon?.box}"
|
||||||
|
>
|
||||||
|
{@html displayIcon?.svg}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
svg {
|
||||||
|
fill: var(--iconColor);
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
&:hover,
|
||||||
|
&.hover {
|
||||||
|
animation: colorFade 0.25s ease-in-out;
|
||||||
|
fill: var(--iconHoverColor);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes colorFade {
|
||||||
|
0% {
|
||||||
|
fill: var(--iconColor);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
fill: var(--iconHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<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}
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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' }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import InstallationInstructions from '$lib/InstallationInstructions.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InstallationInstructions />
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
export const templates = writable([]);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 599 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||