diff --git a/packages/dashboard/src/components/Form/validators.ts b/packages/dashboard/src/components/Form/validators.ts
index cdba3603..09189155 100644
--- a/packages/dashboard/src/components/Form/validators.ts
+++ b/packages/dashboard/src/components/Form/validators.ts
@@ -1,5 +1,5 @@
import validator from 'validator';
-import { AppConfig, FieldTypes } from '../../core/types';
+import { AppConfig, FieldTypes } from '@runtipi/common';
const validateField = (field: AppConfig['form_fields'][0], value: string): string | undefined => {
if (field.required && !value) {
diff --git a/packages/dashboard/src/components/Layout/Menu.tsx b/packages/dashboard/src/components/Layout/Menu.tsx
index bf177b7c..6f294ae6 100644
--- a/packages/dashboard/src/components/Layout/Menu.tsx
+++ b/packages/dashboard/src/components/Layout/Menu.tsx
@@ -1,5 +1,5 @@
import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
-import { FaRegMoon } from 'react-icons/fa';
+import { FaAppStore, FaRegMoon } from 'react-icons/fa';
import { FiLogOut } from 'react-icons/fi';
import Package from '../../../package.json';
import { Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
@@ -45,7 +45,8 @@ const SideMenu: React.FC = () => {
{renderMenuItem('Dashboard', '', AiOutlineDashboard)}
- {renderMenuItem('Apps', 'apps', AiOutlineAppstore)}
+ {renderMenuItem('My Apps', 'apps', AiOutlineAppstore)}
+ {renderMenuItem('App Store', 'app-store', FaAppStore)}
{renderMenuItem('Settings', 'settings', AiOutlineSetting)}
diff --git a/packages/dashboard/src/components/Markdown/Markdown.tsx b/packages/dashboard/src/components/Markdown/Markdown.tsx
new file mode 100644
index 00000000..1bb97474
--- /dev/null
+++ b/packages/dashboard/src/components/Markdown/Markdown.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkBreaks from 'remark-breaks';
+import remarkGfm from 'remark-gfm';
+import remarkMdx from 'remark-mdx';
+
+const Markdown: React.FC<{ children: string; className: string }> = ({ children, className }) => {
+ return (
+
,
+ h2: (props) => ,
+ h3: (props) => ,
+ ul: (props) => ,
+ img: (props) => (
+
+
![]()
+
+ ),
+ p: (props) => ,
+ a: (props) => ,
+ div: (props) => ,
+ }}
+ remarkPlugins={[remarkBreaks, remarkGfm, remarkMdx]}
+ >
+ {children}
+
+ );
+};
+
+export default Markdown;
diff --git a/packages/dashboard/src/constants/apps.ts b/packages/dashboard/src/constants/apps.ts
index 894ebc73..da56fd8d 100644
--- a/packages/dashboard/src/constants/apps.ts
+++ b/packages/dashboard/src/constants/apps.ts
@@ -1,107 +1,22 @@
-import validator from 'validator';
+// import validator from 'validator';
-interface IFormField {
- name: string;
- type: string;
- required: boolean;
- description?: string;
- placeholder?: string;
- validate?: (value: string) => boolean;
-}
+// interface IFormField {
+// name: string;
+// type: string;
+// required: boolean;
+// description?: string;
+// placeholder?: string;
+// validate?: (value: string) => boolean;
+// }
-interface IAppConfig {
- id: string;
- name: string;
- description: string;
- logo: string;
- url: string;
- color: string;
- install_form: { fields: IFormField[] };
-}
+// interface IAppConfig {
+// id: string;
+// name: string;
+// description: string;
+// logo: string;
+// url: string;
+// color: string;
+// install_form: { fields: IFormField[] };
+// }
-const APP_ANONADDY: IAppConfig = {
- id: 'anonaddy',
- name: 'Anonaddy',
- description: 'Create Unlimited Email Aliases For Free',
- url: 'https://anonaddy.com/',
- color: '#00a8ff',
- logo: 'https://anonaddy.com/favicon.ico',
- install_form: {
- fields: [
- {
- name: 'API Key',
- type: 'text',
- placeholder: 'API Key',
- required: true,
- validate: (value: string) => validator.isBase64(value),
- },
- {
- name: 'Return Path',
- type: 'text',
- description: 'The email address that bounces will be sent to',
- placeholder: 'Return Path',
- required: false,
- validate: (value: string) => validator.isEmail(value),
- },
- {
- name: 'Admin Username',
- type: 'text',
- description: 'The username of the admin user',
- placeholder: 'Admin Username',
- required: true,
- },
- {
- name: 'Enable Registration',
- type: 'boolean',
- description: 'Allow users to register',
- placeholder: 'Enable Registration',
- required: false,
- },
- {
- name: 'Domain',
- type: 'text',
- description: 'The domain that will be used for the email address',
- placeholder: 'Domain',
- required: true,
- validate: (value: string) => validator.isFQDN(value),
- },
- {
- name: 'Hostname',
- type: 'text',
- description: 'The hostname that will be used for the email address',
- placeholder: 'Hostname',
- required: true,
- validate: (value: string) => validator.isFQDN(value),
- },
- {
- name: 'Secret',
- type: 'text',
- description: 'The secret that will be used for the email address',
- placeholder: 'Secret',
- required: true,
- },
- {
- name: 'From Name',
- type: 'text',
- description: 'The name that will be used for the email address',
- placeholder: 'From Name',
- required: true,
- validate: (value: string) => validator.isLength(value, { min: 1, max: 64 }),
- },
- {
- name: 'From Address',
- type: 'text',
- description: 'The email address that will be used for the email address',
- placeholder: 'From Address',
- required: true,
- validate: (value: string) => validator.isEmail(value),
- },
- ],
- },
-};
-
-const APPS_CONFIG = {
- available: [APP_ANONADDY],
-};
-
-export default APPS_CONFIG;
+export {};
diff --git a/packages/dashboard/src/core/types.ts b/packages/dashboard/src/core/types.ts
index 1c1069a9..20234f7c 100644
--- a/packages/dashboard/src/core/types.ts
+++ b/packages/dashboard/src/core/types.ts
@@ -1,57 +1,9 @@
-export enum FieldTypes {
- text = 'text',
- password = 'password',
- email = 'email',
- number = 'number',
- fqdn = 'fqdn',
- ip = 'ip',
- fqdnip = 'fqdnip',
- url = 'url',
-}
-
-interface FormField {
- type: FieldTypes;
- label: string;
- max?: number;
- min?: number;
- hint?: string;
- required?: boolean;
- env_variable: string;
-}
-
-export interface AppConfig {
- id: string;
- port: number;
- requirements?: {
- ports?: number[];
- };
- name: string;
- description: string;
- version: string;
- image: string;
- form_fields: Record
;
- short_desc: string;
- author: string;
- source: string;
- installed: boolean;
- status: AppStatus;
-}
-
export enum RequestStatus {
SUCCESS = 'SUCCESS',
ERROR = 'ERROR',
LOADING = 'LOADING',
}
-export enum AppStatus {
- RUNNING = 'running',
- STOPPED = 'stopped',
- INSTALLING = 'installing',
- UNINSTALLING = 'uninstalling',
- STOPPING = 'stopping',
- STARTING = 'starting',
-}
-
export interface IUser {
name: string;
email: string;
diff --git a/packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx b/packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx
new file mode 100644
index 00000000..8806ea9a
--- /dev/null
+++ b/packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx
@@ -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 = ({ data, onSearch, onSelectCategories }) => {
+ const handleSearch = (e: React.ChangeEvent) => onSearch(e.target.value);
+
+ return (
+
+
+
+ {data.map((app) => (
+
+ ))}
+
+
+ );
+};
+
+export default AppStoreTable;
diff --git a/packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx b/packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx
new file mode 100644
index 00000000..3212336b
--- /dev/null
+++ b/packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx
@@ -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 (
+
+
+
+
+
{limitText(app.name, 20)}
+
{limitText(app.short_desc, 45)}
+ {app.categories?.map((category) => (
+
+ {category}
+
+ ))}
+
+
+
+ );
+};
+
+export default AppStoreTile;
diff --git a/packages/dashboard/src/modules/AppStore/components/CategorySelect.tsx b/packages/dashboard/src/modules/AppStore/components/CategorySelect.tsx
new file mode 100644
index 00000000..4aaaea92
--- /dev/null
+++ b/packages/dashboard/src/modules/AppStore/components/CategorySelect.tsx
@@ -0,0 +1,45 @@
+import { useColorModeValue } from '@chakra-ui/react';
+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 = ({ onSelect }) => {
+ const bg = useColorModeValue('white', '#1a202c');
+
+ 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 (
+