Update tests
|
@ -9,7 +9,7 @@
|
|||
[](https://discord.gg/Bu9qEPnHsc)
|
||||
[](https://matrix.to/#/#runtipi:matrix.org)
|
||||
|
||||

|
||||

|
||||
> ⚠️ Tipi is still at an early stage of development and issues are to be expected. Feel free to open an issue or pull request if you find a bug.
|
||||
|
||||
Tipi is a personal homeserver orchestrator. It is running docker containers under the hood and provides a simple web interface to manage them. Every service comes with an opinionated configuration in order to remove the need for manual configuration and network setup.
|
||||
|
@ -92,3 +92,4 @@ Tipi is licensed under the GNU General Public License v3.0. TL;DR — You may co
|
|||
- [Matrix](https://matrix.to/#/#runtipi:matrix.org)<br />
|
||||
- [Twitter](https://twitter.com/runtipi)
|
||||
- [Telegram](https://t.me/+72-y10MnLBw2ZGI0)
|
||||
- [Discord](https://discord.gg/Bu9qEPnHsc)
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8104,
|
||||
"id": "adguard",
|
||||
"categories": ["network", "security", "featured"],
|
||||
"categories": ["network", "security"],
|
||||
"description": "Adguard is the best way to get rid of annoying ads and online tracking and protect your computer from malware. Make your web surfing fast, safe and ad-free.",
|
||||
"short_desc": "World's most advanced adblocker!",
|
||||
"author": "ArneNaessens",
|
||||
"source": "https://github.com/AdguardTeam",
|
||||
"image": "https://avatars.githubusercontent.com/u/8361145?s=200&v=4",
|
||||
"image": "/logos/apps/adguard.jpg",
|
||||
"requirements": {
|
||||
"ports": [53]
|
||||
},
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
Network-wide ads & trackers blocking DNS server
|
||||
Network-wide ads & trackers blocking *DNS server*
|
||||
|
||||
AdGuard Home is a network-wide software for blocking ads and tracking. After you set it up, it'll cover all your home devices, and you won't need any client-side software for that. Learn more on our official Github repository.
|
||||
|
||||
> A block quote with ~strikethrough~ and a URL: https://reactjs.org.
|
||||
|
||||
* Lists
|
||||
* [ ] todo
|
||||
* [x] done
|
||||
|
||||
A table:
|
||||
|
||||
| a | b |
|
||||
| - | - |
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8100,
|
||||
"id": "calibre-web",
|
||||
"categories": ["books"],
|
||||
"description": "On the initial setup screen, enter /books as your calibre library location. \n Default admin login: Username: admin Password: admin123",
|
||||
"short_desc": "Calibre-web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing Calibre database.",
|
||||
"author": "https://github.com/janeczku/",
|
||||
"source": "https://github.com/janeczku/calibre-web",
|
||||
"image": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/calibre-web-icon.png",
|
||||
"image": "/logos/apps/calibre-web.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"available": true,
|
||||
"port": 8101,
|
||||
"id": "code-server",
|
||||
"categories": ["development"],
|
||||
"description": "",
|
||||
"short_desc": "Code-server is VS Code running on a remote server, accessible through the browser.",
|
||||
"author": "https://github.com/coder",
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8096,
|
||||
"id": "filebrowser",
|
||||
"categories": ["utilities"],
|
||||
"description": "Reliable and Performant File Management Desktop Sync and File Sharing\n Default credentials: admin / admin",
|
||||
"short_desc": "Access your homeserver files from your browser",
|
||||
"author": "",
|
||||
"website": "https://filebrowser.org/",
|
||||
"source": "https://github.com/filebrowser/filebrowser",
|
||||
"image": "https://avatars.githubusercontent.com/u/35781395?s=200&v=4",
|
||||
"image": "/logos/apps/filebrowser.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8086,
|
||||
"id": "freshrss",
|
||||
"categories": ["utilities"],
|
||||
"description": "FreshRSS is a self-hosted RSS feed aggregator like Leed or Kriss Feed.\nIt is lightweight, easy to work with, powerful, and customizable.\n\nIt is a multi-user application with an anonymous reading mode. It supports custom tags. There is an API for (mobile) clients, and a Command-Line Interface.\n\nThanks to the WebSub standard (formerly PubSubHubbub), FreshRSS is able to receive instant push notifications from compatible sources, such as Mastodon, Friendica, WordPress, Blogger, FeedBurner, etc.\n\nFreshRSS natively supports basic Web scraping, based on XPath, for Web sites not providing any RSS / Atom feed.\n\nFinally, it supports extensions for further tuning.",
|
||||
"short_desc": "A free, self-hostable aggregator… ",
|
||||
"author": "https://freshrss.org/",
|
||||
"source": "https://github.com/FreshRSS/FreshRSS",
|
||||
"image": "https://avatars.githubusercontent.com/u/9414285?s=200&v=4",
|
||||
"image": "/logos/apps/freshrss.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"port": 8108,
|
||||
"available": true,
|
||||
"id": "gitea",
|
||||
"categories": ["development"],
|
||||
"description": "Gitea is a painless self-hosted Git service. It is similar to GitHub, Bitbucket, and GitLab. Gitea is a fork of Gogs. See the Gitea Announcement blog post to read about the justification for a fork.",
|
||||
"short_desc": "Gitea - Git with a cup of tea · A painless self-hosted Git service. · Cross-platform · Easy to install · Lightweight · Open Source.",
|
||||
"author": "go-gitea",
|
||||
"source": "https://github.com/go-gitea/gitea",
|
||||
"image": "https://avatars.githubusercontent.com/u/12724356?s=200&v=4",
|
||||
"image": "/logos/apps/gitea.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8102,
|
||||
"id": "homarr",
|
||||
"categories": ["utilities"],
|
||||
"description": "A homepage for your server.",
|
||||
"short_desc": "Homarr is a simple and lightweight homepage for your server, that helps you easily access all of your services in one place.",
|
||||
"author": "ajnart",
|
||||
"source": "https://github.com/ajnart/homarr",
|
||||
"website": "https://discord.gg/C2WTXkzkwK",
|
||||
"image": "https://raw.githubusercontent.com/ajnart/homarr/master/public/imgs/logo.png",
|
||||
"image": "/logos/apps/homarr.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8123,
|
||||
"id": "homeassistant",
|
||||
"categories": ["automation"],
|
||||
"description": "Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.",
|
||||
"short_desc": "Open source home automation that puts local control and privacy first",
|
||||
"author": "ArneNaessens",
|
||||
"source": "https://github.com/home-assistant/core",
|
||||
"image": "https://avatars.githubusercontent.com/u/13844975?s=200&v=4",
|
||||
"image": "/logos/apps/homeassistant.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"available": true,
|
||||
"port": 8095,
|
||||
"id": "invidious",
|
||||
"categories": ["media", "social"],
|
||||
"description": "Invidious is an open source alternative front-end to YouTube.",
|
||||
"short_desc": "An alternative front-end to YouTube",
|
||||
"author": "iv-org",
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
"id": "jackett",
|
||||
"description": "Jackett works as a proxy server: it translates queries from apps (Sonarr, Radarr, SickRage, CouchPotato, Mylar3, Lidarr, DuckieTV, qBittorrent, Nefarious etc.) into tracker-site-specific http queries, parses the html or json response, and then sends results back to the requesting software. This allows for getting recent uploads (like RSS) and performing searches.",
|
||||
"short_desc": "API Support for your favorite torrent trackers ",
|
||||
"categories": ["media", "utilities"],
|
||||
"author": "",
|
||||
"source": "https://github.com/Jackett/Jackett",
|
||||
"image": "https://avatars.githubusercontent.com/u/15383019?s=200&v=4",
|
||||
"image": "/logos/apps/jackett.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"available": true,
|
||||
"port": 8091,
|
||||
"id": "jellyfin",
|
||||
"categories": ["media"],
|
||||
"description": "Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest!",
|
||||
"short_desc": "A media server for your home collection",
|
||||
"author": "jellyfin.org",
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8099,
|
||||
"id": "joplin",
|
||||
"categories": ["utilities"],
|
||||
"description": "Default credentials: admin@localhost / admin",
|
||||
"short_desc": "Note taking and to-do application with synchronisation",
|
||||
"author": "https://github.com/laurent22",
|
||||
"source": "https://github.com/laurent22/joplin",
|
||||
"website": "https://joplinapp.org",
|
||||
"image": "https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png",
|
||||
"image": "/logos/apps/joplin.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8105,
|
||||
"id": "libreddit",
|
||||
"categories": ["social"],
|
||||
"description": "LibReddit is a bloat free reddit frontend written in Rust, no ads, no tracking and strong Content Security Policy prevents any request from going to reddit, everything is proxied.",
|
||||
"short_desc": "Browse reddit without problems!",
|
||||
"author": "spikecodes",
|
||||
"source": "https://github.com/spikecodes/libreddit",
|
||||
"image": "https://raw.githubusercontent.com/spikecodes/libreddit/master/static/logo.png",
|
||||
"image": "/logos/apps/libreddit.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8094,
|
||||
"id": "n8n",
|
||||
"categories": ["automation"],
|
||||
"description": "n8n is an extendable workflow automation tool. With a fair-code distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.",
|
||||
"short_desc": "Workflow Automation Tool. Alternative to Zapier",
|
||||
"author": "n8n.io",
|
||||
"source": "https://github.com/n8n-io/n8n",
|
||||
"website": "https://n8n.io/",
|
||||
"image": "https://avatars.githubusercontent.com/u/45487711?s=200&v=4",
|
||||
"image": "/logos/apps/n8n.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"available": true,
|
||||
"port": 8083,
|
||||
"id": "nextcloud",
|
||||
"categories": ["data"],
|
||||
"description": "Nextcloud is a self-hosted, open source, and fully-featured cloud storage solution for your personal files, office documents, and photos.",
|
||||
"short_desc": "Productivity platform that keeps you in control",
|
||||
"author": "Nextcloud GmbH",
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8106,
|
||||
"id": "nitter",
|
||||
"categories": ["social"],
|
||||
"description": "A free and open source alternative Twitter front-end focused on privacy and performance.",
|
||||
"short_desc": "Twitter without annoyances!",
|
||||
"author": "zedeus",
|
||||
"source": "https://github.com/zedeus/nitter",
|
||||
"image": "https://raw.githubusercontent.com/zedeus/nitter/master/public/favicon.ico",
|
||||
"image": "/logos/apps/nitter.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"port": 8111,
|
||||
"available": true,
|
||||
"id": "nodered",
|
||||
"categories": ["automation"],
|
||||
"description": "Node-RED is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways. It provides a browser-based editor that makes it easy to wire together flows using the wide range of nodes in the palette that can be deployed to its runtime in a single-click.",
|
||||
"short_desc": "Low-code programming for event-driven applications",
|
||||
"author": "node-red",
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"port": 8110,
|
||||
"available": true,
|
||||
"id": "photoprism",
|
||||
"categories": ["photography"],
|
||||
"description": "PhotoPrism® is an AI-Powered Photos App for the Decentralized Web. It makes use of the latest technologies to tag and find pictures automatically without getting in your way. You can run it at home, on a private server, or in the cloud. Default username: admin",
|
||||
"short_desc": "AI-Powered Photos App for the Decentralized Web. We are on a mission to protect your freedom and privacy.",
|
||||
"author": "PhotoPrism",
|
||||
"source": "https://github.com/photoprism/photoprism",
|
||||
"image": "https://avatars.githubusercontent.com/u/32436079?s=200&v=4",
|
||||
"image": "/logos/apps/photoprism.jpg",
|
||||
"form_fields": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
"ports": [53]
|
||||
},
|
||||
"id": "pihole",
|
||||
"categories": ["network", "security"],
|
||||
"description": "The Pi-hole® is a DNS sinkhole that protects your devices from unwanted content without installing any client-side software.",
|
||||
"short_desc": "A black hole for Internet advertisements",
|
||||
"author": "pi-hole.net",
|
||||
"source": "https://github.com/pi-hole/pi-hole",
|
||||
"image": "https://avatars.githubusercontent.com/u/16827203?s=200&v=4",
|
||||
"image": "/logos/apps/pihole.jpg",
|
||||
"form_fields": {
|
||||
"password": {
|
||||
"type": "password",
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8109,
|
||||
"id": "prowlarr",
|
||||
"categories": ["media", "utilities"],
|
||||
"description": "Prowlarr is an indexer manager/proxy built on the popular *arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports management of both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Lidarr, Mylar3, Radarr, Readarr, and Sonarr offering complete management of your indexers with no per app Indexer setup required (we do it all).",
|
||||
"short_desc": "A torrent/usenet indexer manager/proxy",
|
||||
"author": "Prowlarr",
|
||||
"source": "https://github.com/Prowlarr/Prowlarr/",
|
||||
"image": "https://prowlarr.com/logo/256.png",
|
||||
"image": "/logos/apps/prowlarr.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8088,
|
||||
"id": "radarr",
|
||||
"categories": ["media", "utilities"],
|
||||
"description": "Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available. Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.",
|
||||
"short_desc": "Movie collection manager for Usenet and BitTorrent users.",
|
||||
"author": "radarr.video",
|
||||
"source": "https://github.com/Radarr/Radarr",
|
||||
"image": "https://avatars.githubusercontent.com/u/25025331?s=200&v=4",
|
||||
"image": "/logos/apps/radarr.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8098,
|
||||
"id": "sonarr",
|
||||
"categories": ["media", "utilities"],
|
||||
"description": "Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.",
|
||||
"short_desc": "TV show manager for Usenet and BitTorrent",
|
||||
"author": "sonarr.tv",
|
||||
"source": "https://github.com/Sonarr/Sonarr",
|
||||
"image": "https://avatars.githubusercontent.com/u/1082903?s=200&v=4",
|
||||
"image": "/logos/apps/sonarr.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8090,
|
||||
"id": "syncthing",
|
||||
"categories": ["data", "utilities"],
|
||||
"description": "Syncthing is a peer-to-peer continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.\n\nInstall the Syncthing app on your Umbrel and pair it with the Syncthing app on your phone or computer for a self hosted peer-to-peer backup solution.",
|
||||
"short_desc": "Peer-to-peer file synchronization between your devices",
|
||||
"author": "The Syncthing Foundation",
|
||||
"source": "https://github.com/syncthing",
|
||||
"website": "https://syncthing.net",
|
||||
"image": "https://avatars.githubusercontent.com/u/7628018?s=200&v=4",
|
||||
"image": "/logos/apps/syncthing.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"available": true,
|
||||
"port": 8093,
|
||||
"id": "tailscale",
|
||||
"categories": ["featured"],
|
||||
"categories": ["network", "security"],
|
||||
"description": "Zero config VPN. Installs on any device in minutes, manages firewall rules for you, and works from anywhere.",
|
||||
"short_desc": "The easiest, most secure way to use WireGuard and 2FA.",
|
||||
"author": "© Tailscale Inc.",
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
"available": true,
|
||||
"port": 8181,
|
||||
"id": "tautulli",
|
||||
"categories": ["media", "utilities"],
|
||||
"description": "Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched. The only thing missing is \"why they watched it\", but who am I to question your 42 plays of Frozen. All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else.",
|
||||
"short_desc": "A Python based monitoring and tracking tool for Plex Media Server.",
|
||||
"author": "JonnyWong16",
|
||||
"source": "https://github.com/Tautulli/Tautulli",
|
||||
"image": "https://tautulli.com/images/logo-circle.png",
|
||||
"image": "/logos/apps/tautulli.jpg",
|
||||
"form_fields": {}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"ports": [51413]
|
||||
},
|
||||
"id": "transmission",
|
||||
"categories": ["utilities"],
|
||||
"description": "Transmission is a fast, easy, and free BitTorrent client.",
|
||||
"short_desc": "Fast, easy, and free BitTorrent client",
|
||||
"author": "Transmission Project",
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8107,
|
||||
"id": "vaultwarden",
|
||||
"categories": ["utilities"],
|
||||
"description": "Alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients, perfect for self-hosted deployment where running the official resource-heavy service might not be ideal.",
|
||||
"short_desc": "All your passwords in your control!",
|
||||
"author": "Daniel García",
|
||||
"source": "https://github.com/dani-garcia/vaultwarden",
|
||||
"image": "https://raw.githubusercontent.com/dani-garcia/vaultwarden/b636d20c6475bfb1b36561cb95812faee26ea7db/resources/vaultwarden-icon.svg",
|
||||
"image": "/logos/apps/vaultwarden.jpg",
|
||||
"form_fields": {
|
||||
"admin_password": {
|
||||
"type": "password",
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
"ports": [51820]
|
||||
},
|
||||
"id": "wg-easy",
|
||||
"categories": ["network"],
|
||||
"description": "Access your homeserver from anywhere even on your mobile device. Wireguard-easy is a simple tool to configure and manage Wireguard VPN servers. It is written in Go and uses the official Wireguard client. You have to open and redirect port 51820 to your homeserver in order to connect.",
|
||||
"short_desc": "VPN server for your homeserver",
|
||||
"author": "WeeJeWel",
|
||||
"source": "https://github.com/WeeJeWel/wg-easy/",
|
||||
"image": "https://avatars.githubusercontent.com/u/13991055?s=200&v=4",
|
||||
"image": "/logos/apps/wireguard.jpg",
|
||||
"form_fields": {
|
||||
"host": {
|
||||
"type": "fqdnip",
|
||||
|
|
|
@ -11,4 +11,6 @@ export const APP_CATEGORIES = [
|
|||
{ name: 'Photography', id: AppCategoriesEnum.PHOTOGRAPHY, icon: 'FaCamera' },
|
||||
{ name: 'Security', id: AppCategoriesEnum.SECURITY, icon: 'FaShieldAlt' },
|
||||
{ name: 'Featured', id: AppCategoriesEnum.FEATURED, icon: 'FaStar' },
|
||||
{ name: 'Books', id: AppCategoriesEnum.BOOKS, icon: 'FaBook' },
|
||||
{ name: 'Data', id: AppCategoriesEnum.DATA, icon: 'FaDatabase' },
|
||||
];
|
||||
|
|
|
@ -8,6 +8,8 @@ export enum AppCategoriesEnum {
|
|||
PHOTOGRAPHY = 'photography',
|
||||
SECURITY = 'security',
|
||||
FEATURED = 'featured',
|
||||
BOOKS = 'books',
|
||||
DATA = 'data',
|
||||
}
|
||||
|
||||
export enum FieldTypes {
|
||||
|
|
|
@ -26,6 +26,10 @@
|
|||
"react-dom": "18.1.0",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-select": "^5.3.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"swr": "^1.3.0",
|
||||
"systeminformation": "^5.11.9",
|
||||
"validator": "^13.7.0",
|
||||
|
|
BIN
packages/dashboard/public/logos/apps/adguard.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
packages/dashboard/public/logos/apps/adguard.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
packages/dashboard/public/logos/apps/calibre-web.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
packages/dashboard/public/logos/apps/filebrowser.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
packages/dashboard/public/logos/apps/freshrss.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
packages/dashboard/public/logos/apps/gitea.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
packages/dashboard/public/logos/apps/homarr.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
packages/dashboard/public/logos/apps/homeassistant.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
packages/dashboard/public/logos/apps/jackett.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
packages/dashboard/public/logos/apps/joplin.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
packages/dashboard/public/logos/apps/libreddit.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
packages/dashboard/public/logos/apps/n8n.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
packages/dashboard/public/logos/apps/nitter.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
packages/dashboard/public/logos/apps/photoprism.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
packages/dashboard/public/logos/apps/pihole.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
packages/dashboard/public/logos/apps/prowlarr.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
packages/dashboard/public/logos/apps/radarr.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
packages/dashboard/public/logos/apps/sonarr.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
packages/dashboard/public/logos/apps/syncthing.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
packages/dashboard/public/logos/apps/tautulli.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/dashboard/public/logos/apps/vaultwarden.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
packages/dashboard/public/logos/apps/wireguard.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
16
packages/dashboard/src/components/AppLogo/AppLogo.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
const AppLogo: React.FC<{ src: string; size?: number; className?: string; alt?: string }> = ({ src, size = 80, className = '', alt = '' }) => {
|
||||
return (
|
||||
<div aria-label={alt} className={className} style={{ width: size, height: size }}>
|
||||
<svg width={size} height={size} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0" maskUnits="userSpaceOnUse" x="0" y="0" width="200" height="200">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M0 100C0 0 0 0 100 0S200 0 200 100 200 200 100 200 0 200 0 100" fill="white" />
|
||||
</mask>
|
||||
<image href={src} mask="url(#mask0)" width="200" height="200" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLogo;
|
1
packages/dashboard/src/components/AppLogo/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './AppLogo';
|
|
@ -1,9 +1,10 @@
|
|||
import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Box, SlideFade, useColorModeValue } from '@chakra-ui/react';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { FiChevronRight } from 'react-icons/fi';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import AppStatus from './AppStatus';
|
||||
import AppLogo from '../AppLogo/AppLogo';
|
||||
|
||||
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||
const bg = useColorModeValue('white', '#1a202c');
|
||||
|
@ -12,7 +13,7 @@ const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
|||
<Link href={`/apps/${app.id}`} passHref>
|
||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||
<Box minWidth={400} bg={bg} className="flex flex-1 border-2 drop-shadow-sm rounded-lg p-3 items-center cursor-pointer group hover:drop-shadow-md transition-all">
|
||||
<Image alt={`${app.name} logo`} className="rounded-md drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} width={100} height={100} />
|
||||
<AppLogo alt={`${app.name} logo`} className="drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} size={100} />
|
||||
<div className="mr-3 flex-1">
|
||||
<h3 className="font-bold text-xl">{app.name}</h3>
|
||||
<span>{app.short_desc}</span>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { Flex, Input, SimpleGrid } from '@chakra-ui/react';
|
||||
import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
|
||||
import React from 'react';
|
||||
import { SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
import AppStoreTile from './AppStoreTile';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
interface IProps {
|
||||
data: AppConfig[];
|
||||
onSearch: (value: string) => void;
|
||||
onSelectCategories: (value: AppCategoriesEnum[]) => void;
|
||||
onSortBy: (value: SortableColumns) => void;
|
||||
onChangeDirection: (value: SortDirection) => void;
|
||||
}
|
||||
|
||||
const AppStoreTable: React.FC<IProps> = ({ data, onSearch, onSelectCategories }) => {
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => onSearch(e.target.value);
|
||||
|
||||
return (
|
||||
<Flex className="flex-col">
|
||||
<div className="flex">
|
||||
<div className="flex-1 mr-2">
|
||||
<Input placeholder="Search app..." onChange={handleSearch} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<CategorySelect onSelect={onSelectCategories} />
|
||||
</div>
|
||||
</div>
|
||||
<SimpleGrid className="flex-1" minChildWidth="280px" spacing="20px">
|
||||
{data.map((app) => (
|
||||
<AppStoreTile key={app.id} app={app} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppStoreTable;
|
|
@ -0,0 +1,27 @@
|
|||
import { Tag, TagLabel } from '@chakra-ui/react';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import AppLogo from '../../../components/AppLogo/AppLogo';
|
||||
import { colorSchemeForCategory, limitText } from '../helpers/table.helpers';
|
||||
|
||||
const AppStoreTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||
return (
|
||||
<Link href={`/app-store/${app.id}`} passHref>
|
||||
<div key={app.id} className="p-2 rounded-md app-store-tile flex items-center group">
|
||||
<AppLogo src={app.image} className="group-hover:scale-105 transition-all" />
|
||||
<div className="ml-2">
|
||||
<div className="font-bold">{limitText(app.name, 20)}</div>
|
||||
<div className="text-sm mb-1">{limitText(app.short_desc, 45)}</div>
|
||||
{app.categories?.map((category) => (
|
||||
<Tag colorScheme={colorSchemeForCategory[category]} className="mr-1" borderRadius="full" key={`${app.id}-${category}`} size="sm" variant="solid">
|
||||
<TagLabel>{category}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppStoreTile;
|
|
@ -0,0 +1,37 @@
|
|||
import { AppCategoriesEnum, APP_CATEGORIES } from '@runtipi/common';
|
||||
import React from 'react';
|
||||
import Select, { Options } from 'react-select';
|
||||
|
||||
interface IProps {
|
||||
onSelect: (value: AppCategoriesEnum[]) => void;
|
||||
}
|
||||
|
||||
type OptionsType = Options<{ value: AppCategoriesEnum; label: string }>;
|
||||
|
||||
const CategorySelect: React.FC<IProps> = ({ onSelect }) => {
|
||||
const options: OptionsType = APP_CATEGORIES.map((category) => ({
|
||||
value: category.id,
|
||||
label: category.name,
|
||||
}));
|
||||
|
||||
const handleChange = (values: OptionsType) => {
|
||||
const categories = values.map((category) => category.value);
|
||||
onSelect(categories);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
styles={{ control: (base) => ({ ...base, borderColor: 'gray.600', height: 40 }), placeholder: (base) => ({ ...base, color: 'gray' }) }}
|
||||
onChange={handleChange}
|
||||
defaultValue={[]}
|
||||
isMulti
|
||||
name="categories"
|
||||
options={options as any}
|
||||
placeholder="Filter by category..."
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategorySelect;
|
|
@ -2,21 +2,45 @@ import { Flex } from '@chakra-ui/react';
|
|||
import { AppCategoriesEnum } from '@runtipi/common';
|
||||
import React from 'react';
|
||||
import { useAppsStore } from '../../../state/appsStore';
|
||||
import FeaturedApps from '../components/FeaturedApps';
|
||||
import AppStoreTable from '../components/AppStoreTable';
|
||||
import { sortTable } from '../helpers/table.helpers';
|
||||
import { SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
|
||||
function nonNullable<T>(value: T): value is NonNullable<T> {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
// function nonNullable<T>(value: T): value is NonNullable<T> {
|
||||
// return value !== null && value !== undefined;
|
||||
// }
|
||||
|
||||
const AppStoreContainer = () => {
|
||||
const { apps } = useAppsStore();
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [categories, setCategories] = React.useState<AppCategoriesEnum[]>([]);
|
||||
const [sort, setSort] = React.useState<SortableColumns>('name');
|
||||
const [sortDirection, setSortDirection] = React.useState<SortDirection>('asc');
|
||||
|
||||
const featuredApps = apps.map((app) => (app.categories?.includes(AppCategoriesEnum.FEATURED) ? app : null)).filter(nonNullable);
|
||||
const tableData = React.useMemo(() => {
|
||||
return sortTable(apps, sort, sortDirection, categories, search);
|
||||
}, [categories, apps, sort, sortDirection, search]);
|
||||
|
||||
const handleSearch = React.useCallback((value: string) => {
|
||||
setSearch(value);
|
||||
}, []);
|
||||
|
||||
const handleCategory = React.useCallback((value: AppCategoriesEnum[]) => {
|
||||
setCategories(value);
|
||||
}, []);
|
||||
|
||||
const handleSort = React.useCallback((value: SortableColumns) => {
|
||||
setSort(value);
|
||||
}, []);
|
||||
|
||||
const handleSortDirection = React.useCallback((value: SortDirection) => {
|
||||
setSortDirection(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex className="flex-col">
|
||||
<h1 className="font-bold text-3xl mb-5">App Store</h1>
|
||||
<FeaturedApps apps={featuredApps} />
|
||||
<AppStoreTable data={tableData} onSearch={handleSearch} onSelectCategories={handleCategory} onSortBy={handleSort} onChangeDirection={handleSortDirection} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
|
||||
|
||||
export const sortTable = (data: AppConfig[], col: keyof Pick<AppConfig, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const aVal = a[col];
|
||||
const bVal = b[col];
|
||||
if (aVal < bVal) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aVal > bVal) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (categories.length > 0) {
|
||||
return sortedData.filter((app) => app.categories.some((c) => categories.includes(c))).filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
|
||||
} else {
|
||||
return sortedData.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
|
||||
}
|
||||
};
|
||||
|
||||
export const limitText = (text: string, limit: number) => {
|
||||
return text.length > limit ? `${text.substr(0, limit)}...` : text;
|
||||
};
|
||||
|
||||
export const colorSchemeForCategory: Record<AppCategoriesEnum, string> = {
|
||||
[AppCategoriesEnum.NETWORK]: 'blue',
|
||||
[AppCategoriesEnum.MEDIA]: 'green',
|
||||
[AppCategoriesEnum.AUTOMATION]: 'orange',
|
||||
[AppCategoriesEnum.DEVELOPMENT]: 'purple',
|
||||
[AppCategoriesEnum.UTILITIES]: 'gray',
|
||||
[AppCategoriesEnum.PHOTOGRAPHY]: 'red',
|
||||
[AppCategoriesEnum.SECURITY]: 'yellow',
|
||||
[AppCategoriesEnum.SOCIAL]: 'teal',
|
||||
[AppCategoriesEnum.FEATURED]: 'pink',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
import { AppConfig } from '@runtipi/common';
|
||||
|
||||
export type SortableColumns = keyof Pick<AppConfig, 'name'>;
|
||||
export type SortDirection = 'asc' | 'desc';
|
|
@ -1,4 +1,6 @@
|
|||
import { SlideFade, Image, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { SlideFade, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import React from 'react';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
|
@ -9,6 +11,7 @@ import InstallModal from '../components/InstallModal';
|
|||
import StopModal from '../components/StopModal';
|
||||
import UninstallModal from '../components/UninstallModal';
|
||||
import UpdateModal from '../components/UpdateModal';
|
||||
import AppLogo from '../../../components/AppLogo/AppLogo';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
|
@ -91,11 +94,13 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
window.open(`http://${internalIp}:${app.port}`, '_blank', 'noreferrer');
|
||||
};
|
||||
|
||||
console.log(app.description);
|
||||
|
||||
return (
|
||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||
<div className="flex flex-1 p-4 mt-3 rounded-lg flex-col">
|
||||
<Flex className="flex-col md:flex-row">
|
||||
<Image src={app?.image} height={180} width={180} className="rounded-xl self-center sm:self-auto" alt={app.name} />
|
||||
<AppLogo src={app?.image} size={180} className="self-center sm:self-auto" alt={app.name} />
|
||||
<VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
|
||||
<div className="mt-3 items-center self-center flex flex-col sm:items-start sm:self-start md:mt-0">
|
||||
<h1 className="font-bold text-2xl">{app?.name}</h1>
|
||||
|
@ -124,7 +129,9 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
</VStack>
|
||||
</Flex>
|
||||
<Divider className="mt-5" />
|
||||
<p className="mt-3">{app?.description}</p>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className="mt-3">
|
||||
{app?.description}
|
||||
</ReactMarkdown>
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.onClose} app={app} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.onClose} app={app} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.onClose} app={app} />
|
||||
|
|
39
packages/dashboard/src/pages/app-store/[id].tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { NextPage } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
import Layout from '../../components/Layout';
|
||||
import { useAppsStore } from '../../state/appsStore';
|
||||
import AppDetails from '../../modules/Apps/containers/AppDetails';
|
||||
|
||||
interface IProps {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
|
||||
const { fetchApp, getApp } = useAppsStore((state) => state);
|
||||
const app = getApp(appId);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApp(appId);
|
||||
}, [appId, fetchApp]);
|
||||
|
||||
const breadcrumb = [
|
||||
{ name: 'App Store', href: '/app-store' },
|
||||
{ name: app?.name || '', href: `/app-store/${appId}`, current: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout breadcrumbs={breadcrumb} loading={!app}>
|
||||
{app && <AppDetails app={app} />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
AppDetailsPage.getInitialProps = async (ctx) => {
|
||||
const { query } = ctx;
|
||||
|
||||
const appId = query.id as string;
|
||||
|
||||
return { appId };
|
||||
};
|
||||
|
||||
export default AppDetailsPage;
|
|
@ -6,14 +6,14 @@ import { useAppsStore } from '../../state/appsStore';
|
|||
import { RequestStatus } from '../../core/types';
|
||||
|
||||
const Apps: NextPage = () => {
|
||||
const { fetch, status } = useAppsStore((state) => state);
|
||||
const { fetch, status, apps } = useAppsStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [fetch]);
|
||||
|
||||
return (
|
||||
<Layout loading={status === RequestStatus.LOADING}>
|
||||
<Layout loading={status === RequestStatus.LOADING && apps.length === 0}>
|
||||
<AppStoreContainer />
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,13 @@ const Apps: NextPage = () => {
|
|||
<Layout loading={loading}>
|
||||
<Flex className="flex-col">
|
||||
{installedCount > 0 && <h1 className="font-bold text-3xl mb-5">Your Apps ({installedCount})</h1>}
|
||||
<SimpleGrid minChildWidth="400px" spacing="20px">
|
||||
{installedCount === 0 && (
|
||||
<div>
|
||||
<h1 className="font-bold text-3xl mb-5">No apps installed</h1>
|
||||
<h2>Visit the App Store to install your first app</h2>
|
||||
</div>
|
||||
)}
|
||||
<SimpleGrid minChildWidth="375px" spacing="20px">
|
||||
{installed.map((app) => (
|
||||
<AppTile key={app.name} app={app} />
|
||||
))}
|
||||
|
|
|
@ -35,4 +35,14 @@ a {
|
|||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-store-tile {
|
||||
height: 150px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-store-tile-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
|
|
@ -237,13 +237,13 @@ describe('Get app config', () => {
|
|||
it('Should correctly get app config', async () => {
|
||||
const appconfig = await AppsService.getAppInfo('test-app');
|
||||
|
||||
expect(appconfig).toEqual({ ...testApp, installed: true, status: 'stopped' });
|
||||
expect(appconfig).toEqual({ ...testApp, installed: true, status: 'stopped', description: 'md desc' });
|
||||
});
|
||||
|
||||
it('Should have installed false if app is not installed', async () => {
|
||||
const appconfig = await AppsService.getAppInfo('test-app2');
|
||||
|
||||
expect(appconfig).toEqual({ ...testApp2, installed: false, status: 'stopped' });
|
||||
expect(appconfig).toEqual({ ...testApp2, installed: false, status: 'stopped', description: 'md desc' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ const getAppInfo = async (id: string): Promise<AppConfig> => {
|
|||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
configFile.installed = installed.includes(id);
|
||||
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
|
||||
configFile.description = readFile(`/apps/${id}/metadata/description.md`);
|
||||
|
||||
return configFile;
|
||||
};
|
||||
|
|
739
pnpm-lock.yaml
generated
BIN
screenshots/appstore.png
Normal file
After Width: | Height: | Size: 352 KiB |