Update tests

This commit is contained in:
Nicolas Meienberger 2022-06-07 20:59:30 +02:00
parent f0f1da36ae
commit 11eda1b09e
73 changed files with 1058 additions and 59 deletions

View file

@ -9,7 +9,7 @@
[![Discord](https://img.shields.io/discord/976934649643294750?label=discord&logo=discord)](https://discord.gg/Bu9qEPnHsc)
[![Matrix](https://img.shields.io/matrix/runtipi:matrix.org?label=matrix&logo=matrix)](https://matrix.to/#/#runtipi:matrix.org)
![Preview](https://raw.githubusercontent.com/meienberger/runtipi/develop/screenshots/1.png)
![Preview](https://raw.githubusercontent.com/meienberger/runtipi/develop/screenshots/appstore.png)
> ⚠️ 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)

View file

@ -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]
},

View file

@ -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 |
| - | - |

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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.",

View file

@ -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": {}
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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' },
];

View file

@ -8,6 +8,8 @@ export enum AppCategoriesEnum {
PHOTOGRAPHY = 'photography',
SECURITY = 'security',
FEATURED = 'featured',
BOOKS = 'books',
DATA = 'data',
}
export enum FieldTypes {

View file

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View 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;

View file

@ -0,0 +1 @@
export * from './AppLogo';

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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',
};

View file

@ -0,0 +1,4 @@
import { AppConfig } from '@runtipi/common';
export type SortableColumns = keyof Pick<AppConfig, 'name'>;
export type SortDirection = 'asc' | 'desc';

View file

@ -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} />

View 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;

View file

@ -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>
);

View file

@ -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} />
))}

View file

@ -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;
}

View file

@ -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' });
});
});

View file

@ -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

File diff suppressed because it is too large Load diff

BIN
screenshots/appstore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB