1
0
Fork 0
mirror of https://github.com/pawelmalak/flame.git synced 2025-07-21 20:39:36 +02:00

Merge pull request #123 from pawelmalak/feature

Version 1.7.1
This commit is contained in:
pawelmalak 2021-10-22 17:15:31 +02:00 committed by GitHub
commit 0ec77c33bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1855 additions and 1565 deletions

2
.env
View file

@ -1,3 +1,3 @@
PORT=5005 PORT=5005
NODE_ENV=development NODE_ENV=development
VERSION=1.7.0 VERSION=1.7.1

View file

@ -1,3 +1,11 @@
### v1.7.1 (2021-10-22)
- Fixed search action not being triggered by Numpad Enter
- Added option to change date formatting ([#92](https://github.com/pawelmalak/flame/issues/92))
- Added shortcuts (Esc and double click) to clear search bar ([#100](https://github.com/pawelmalak/flame/issues/100))
- Added Traefik integration ([#102](https://github.com/pawelmalak/flame/issues/102))
- Fixed search bar not redirecting to valid URL if it starts with capital letter ([#118](https://github.com/pawelmalak/flame/issues/118))
- Performance improvements
### v1.7.0 (2021-10-11) ### v1.7.0 (2021-10-11)
- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67)) - Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67))
- Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71)) - Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71))

View file

@ -1 +1 @@
REACT_APP_VERSION=1.7.0 REACT_APP_VERSION=1.7.1

View file

@ -16,7 +16,7 @@ import Settings from './components/Settings/Settings';
import Bookmarks from './components/Bookmarks/Bookmarks'; import Bookmarks from './components/Bookmarks/Bookmarks';
import NotificationCenter from './components/NotificationCenter/NotificationCenter'; import NotificationCenter from './components/NotificationCenter/NotificationCenter';
// Get config pairs from database // Load config
store.dispatch<any>(getConfig()); store.dispatch<any>(getConfig());
// Set theme // Set theme

View file

@ -2,12 +2,13 @@ import classes from './AppCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser, urlParser } from '../../../utility'; import { iconParser, urlParser } from '../../../utility';
import { App } from '../../../interfaces'; import { App, Config, GlobalState } from '../../../interfaces';
import { searchConfig } from '../../../utility'; import { connect } from 'react-redux';
interface ComponentProps { interface ComponentProps {
app: App; app: App;
pinHandler?: Function; pinHandler?: Function;
config: Config;
} }
const AppCard = (props: ComponentProps): JSX.Element => { const AppCard = (props: ComponentProps): JSX.Element => {
@ -29,7 +30,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
<div className={classes.CustomIcon}> <div className={classes.CustomIcon}>
<svg <svg
data-src={`/uploads/${icon}`} data-src={`/uploads/${icon}`}
fill='var(--color-primary)' fill="var(--color-primary)"
className={classes.CustomIcon} className={classes.CustomIcon}
></svg> ></svg>
</div> </div>
@ -41,8 +42,8 @@ const AppCard = (props: ComponentProps): JSX.Element => {
return ( return (
<a <a
href={redirectUrl} href={redirectUrl}
target={searchConfig('appsSameTab', false) ? '' : '_blank'} target={props.config.appsSameTab ? '' : '_blank'}
rel='noreferrer' rel="noreferrer"
className={classes.AppCard} className={classes.AppCard}
> >
<div className={classes.AppCardIcon}>{iconEl}</div> <div className={classes.AppCardIcon}>{iconEl}</div>
@ -54,4 +55,10 @@ const AppCard = (props: ComponentProps): JSX.Element => {
); );
}; };
export default AppCard; const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(AppCard);

View file

@ -1,13 +1,24 @@
import { Fragment, KeyboardEvent, useState, useEffect } from 'react'; import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from 'react-beautiful-dnd';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions'; import {
pinApp,
deleteApp,
reorderApps,
updateConfig,
createNotification,
} from '../../../store/actions';
// Typescript // Typescript
import { App, GlobalState, NewNotification } from '../../../interfaces'; import { App, Config, GlobalState, NewNotification } from '../../../interfaces';
// CSS // CSS
import classes from './AppTable.module.css'; import classes from './AppTable.module.css';
@ -16,11 +27,9 @@ import classes from './AppTable.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
import Table from '../../UI/Table/Table'; import Table from '../../UI/Table/Table';
// Utils
import { searchConfig } from '../../../utility';
interface ComponentProps { interface ComponentProps {
apps: App[]; apps: App[];
config: Config;
pinApp: (app: App) => void; pinApp: (app: App) => void;
deleteApp: (id: number) => void; deleteApp: (id: number) => void;
updateAppHandler: (app: App) => void; updateAppHandler: (app: App) => void;
@ -36,38 +45,44 @@ const AppTable = (props: ComponentProps): JSX.Element => {
// Copy apps array // Copy apps array
useEffect(() => { useEffect(() => {
setLocalApps([...props.apps]); setLocalApps([...props.apps]);
}, [props.apps]) }, [props.apps]);
// Check ordering // Check ordering
useEffect(() => { useEffect(() => {
const order = searchConfig('useOrdering', ''); const order = props.config.useOrdering;
if (order === 'orderId') { if (order === 'orderId') {
setIsCustomOrder(true); setIsCustomOrder(true);
} }
}, []) }, []);
const deleteAppHandler = (app: App): void => { const deleteAppHandler = (app: App): void => {
const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`); const proceed = window.confirm(
`Are you sure you want to delete ${app.name} at ${app.url} ?`
);
if (proceed) { if (proceed) {
props.deleteApp(app.id); props.deleteApp(app.id);
} }
} };
// Support keyboard navigation for actions // Support keyboard navigation for actions
const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => { const keyboardActionHandler = (
e: KeyboardEvent,
app: App,
handler: Function
) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handler(app); handler(app);
} }
} };
const dragEndHanlder = (result: DropResult): void => { const dragEndHanlder = (result: DropResult): void => {
if (!isCustomOrder) { if (!isCustomOrder) {
props.createNotification({ props.createNotification({
title: 'Error', title: 'Error',
message: 'Custom order is disabled' message: 'Custom order is disabled',
}) });
return; return;
} }
@ -81,32 +96,39 @@ const AppTable = (props: ComponentProps): JSX.Element => {
setLocalApps(tmpApps); setLocalApps(tmpApps);
props.reorderApps(tmpApps); props.reorderApps(tmpApps);
} };
return ( return (
<Fragment> <Fragment>
<div className={classes.Message}> <div className={classes.Message}>
{isCustomOrder {isCustomOrder ? (
? <p>You can drag and drop single rows to reorder application</p> <p>You can drag and drop single rows to reorder application</p>
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p> ) : (
} <p>
Custom order is disabled. You can change it in{' '}
<Link to="/settings/other">settings</Link>
</p>
)}
</div> </div>
<DragDropContext onDragEnd={dragEndHanlder}> <DragDropContext onDragEnd={dragEndHanlder}>
<Droppable droppableId='apps'> <Droppable droppableId="apps">
{(provided) => ( {(provided) => (
<Table headers={[ <Table
'Name', headers={['Name', 'URL', 'Icon', 'Actions']}
'URL', innerRef={provided.innerRef}
'Icon', >
'Actions'
]}
innerRef={provided.innerRef}>
{localApps.map((app: App, index): JSX.Element => { {localApps.map((app: App, index): JSX.Element => {
return ( return (
<Draggable key={app.id} draggableId={app.id.toString()} index={index}> <Draggable
key={app.id}
draggableId={app.id.toString()}
index={index}
>
{(provided, snapshot) => { {(provided, snapshot) => {
const style = { const style = {
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', border: snapshot.isDragging
? '1px solid var(--color-accent)'
: 'none',
borderRadius: '4px', borderRadius: '4px',
...provided.draggableProps.style, ...provided.draggableProps.style,
}; };
@ -126,55 +148,77 @@ const AppTable = (props: ComponentProps): JSX.Element => {
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => deleteAppHandler(app)} onClick={() => deleteAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} onKeyDown={(e) =>
tabIndex={0}> keyboardActionHandler(
<Icon icon='mdiDelete' /> e,
app,
deleteAppHandler
)
}
tabIndex={0}
>
<Icon icon="mdiDelete" />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.updateAppHandler(app)} onClick={() => props.updateAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} onKeyDown={(e) =>
tabIndex={0}> keyboardActionHandler(
<Icon icon='mdiPencil' /> e,
app,
props.updateAppHandler
)
}
tabIndex={0}
>
<Icon icon="mdiPencil" />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.pinApp(app)} onClick={() => props.pinApp(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} onKeyDown={(e) =>
tabIndex={0}> keyboardActionHandler(e, app, props.pinApp)
{app.isPinned
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
: <Icon icon='mdiPin' />
} }
tabIndex={0}
>
{app.isPinned ? (
<Icon
icon="mdiPinOff"
color="var(--color-accent)"
/>
) : (
<Icon icon="mdiPin" />
)}
</div> </div>
</td> </td>
)} )}
</tr> </tr>
) );
}} }}
</Draggable> </Draggable>
) );
})} })}
</Table> </Table>
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</Fragment> </Fragment>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
apps: state.app.apps apps: state.app.apps,
} config: state.config.config,
} };
};
const actions = { const actions = {
pinApp, pinApp,
deleteApp, deleteApp,
reorderApps, reorderApps,
updateConfig, updateConfig,
createNotification createNotification,
} };
export default connect(mapStateToProps, actions)(AppTable); export default connect(mapStateToProps, actions)(AppTable);

View file

@ -1,12 +1,14 @@
import { Bookmark, Category } from '../../../interfaces'; import { Bookmark, Category, Config, GlobalState } from '../../../interfaces';
import classes from './BookmarkCard.module.css'; import classes from './BookmarkCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser, urlParser, searchConfig } from '../../../utility'; import { iconParser, urlParser } from '../../../utility';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { connect } from 'react-redux';
interface ComponentProps { interface ComponentProps {
category: Category; category: Category;
config: Config;
} }
const BookmarkCard = (props: ComponentProps): JSX.Element => { const BookmarkCard = (props: ComponentProps): JSX.Element => {
@ -54,7 +56,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
return ( return (
<a <a
href={redirectUrl} href={redirectUrl}
target={searchConfig('bookmarksSameTab', false) ? '' : '_blank'} target={props.config.bookmarksSameTab ? '' : '_blank'}
rel="noreferrer" rel="noreferrer"
key={`bookmark-${bookmark.id}`} key={`bookmark-${bookmark.id}`}
> >
@ -68,4 +70,10 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
); );
}; };
export default BookmarkCard; const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(BookmarkCard);

View file

@ -1,13 +1,30 @@
import { KeyboardEvent, useState, useEffect, Fragment } from 'react'; import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from 'react-beautiful-dnd';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions'; import {
pinCategory,
deleteCategory,
deleteBookmark,
createNotification,
reorderCategories,
} from '../../../store/actions';
// Typescript // Typescript
import { Bookmark, Category, NewNotification } from '../../../interfaces'; import {
Bookmark,
Category,
Config,
GlobalState,
NewNotification,
} from '../../../interfaces';
import { ContentType } from '../Bookmarks'; import { ContentType } from '../Bookmarks';
// CSS // CSS
@ -17,12 +34,10 @@ import classes from './BookmarkTable.module.css';
import Table from '../../UI/Table/Table'; import Table from '../../UI/Table/Table';
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
// Utils
import { searchConfig } from '../../../utility';
interface ComponentProps { interface ComponentProps {
contentType: ContentType; contentType: ContentType;
categories: Category[]; categories: Category[];
config: Config;
pinCategory: (category: Category) => void; pinCategory: (category: Category) => void;
deleteCategory: (id: number) => void; deleteCategory: (id: number) => void;
updateHandler: (data: Category | Bookmark) => void; updateHandler: (data: Category | Bookmark) => void;
@ -38,45 +53,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
// Copy categories array // Copy categories array
useEffect(() => { useEffect(() => {
setLocalCategories([...props.categories]); setLocalCategories([...props.categories]);
}, [props.categories]) }, [props.categories]);
// Check ordering // Check ordering
useEffect(() => { useEffect(() => {
const order = searchConfig('useOrdering', ''); const order = props.config.useOrdering;
if (order === 'orderId') { if (order === 'orderId') {
setIsCustomOrder(true); setIsCustomOrder(true);
} }
}) });
const deleteCategoryHandler = (category: Category): void => { const deleteCategoryHandler = (category: Category): void => {
const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`); const proceed = window.confirm(
`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`
);
if (proceed) { if (proceed) {
props.deleteCategory(category.id); props.deleteCategory(category.id);
} }
} };
const deleteBookmarkHandler = (bookmark: Bookmark): void => { const deleteBookmarkHandler = (bookmark: Bookmark): void => {
const proceed = window.confirm(`Are you sure you want to delete ${bookmark.name}?`); const proceed = window.confirm(
`Are you sure you want to delete ${bookmark.name}?`
);
if (proceed) { if (proceed) {
props.deleteBookmark(bookmark.id, bookmark.categoryId); props.deleteBookmark(bookmark.id, bookmark.categoryId);
} }
} };
const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => { const keyboardActionHandler = (
e: KeyboardEvent,
category: Category,
handler: Function
) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handler(category); handler(category);
} }
} };
const dragEndHanlder = (result: DropResult): void => { const dragEndHanlder = (result: DropResult): void => {
if (!isCustomOrder) { if (!isCustomOrder) {
props.createNotification({ props.createNotification({
title: 'Error', title: 'Error',
message: 'Custom order is disabled' message: 'Custom order is disabled',
}) });
return; return;
} }
@ -90,31 +113,38 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
setLocalCategories(tmpCategories); setLocalCategories(tmpCategories);
props.reorderCategories(tmpCategories); props.reorderCategories(tmpCategories);
} };
if (props.contentType === ContentType.category) { if (props.contentType === ContentType.category) {
return ( return (
<Fragment> <Fragment>
<div className={classes.Message}> <div className={classes.Message}>
{isCustomOrder {isCustomOrder ? (
? <p>You can drag and drop single rows to reorder categories</p> <p>You can drag and drop single rows to reorder categories</p>
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p> ) : (
} <p>
Custom order is disabled. You can change it in{' '}
<Link to="/settings/other">settings</Link>
</p>
)}
</div> </div>
<DragDropContext onDragEnd={dragEndHanlder}> <DragDropContext onDragEnd={dragEndHanlder}>
<Droppable droppableId='categories'> <Droppable droppableId="categories">
{(provided) => ( {(provided) => (
<Table headers={[ <Table headers={['Name', 'Actions']} innerRef={provided.innerRef}>
'Name', {localCategories.map(
'Actions' (category: Category, index): JSX.Element => {
]}
innerRef={provided.innerRef}>
{localCategories.map((category: Category, index): JSX.Element => {
return ( return (
<Draggable key={category.id} draggableId={category.id.toString()} index={index}> <Draggable
key={category.id}
draggableId={category.id.toString()}
index={index}
>
{(provided, snapshot) => { {(provided, snapshot) => {
const style = { const style = {
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', border: snapshot.isDragging
? '1px solid var(--color-accent)'
: 'none',
borderRadius: '4px', borderRadius: '4px',
...provided.draggableProps.style, ...provided.draggableProps.style,
}; };
@ -131,61 +161,80 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
<td className={classes.TableActions}> <td className={classes.TableActions}>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => deleteCategoryHandler(category)} onClick={() =>
onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)} deleteCategoryHandler(category)
tabIndex={0}> }
<Icon icon='mdiDelete' /> onKeyDown={(e) =>
keyboardActionHandler(
e,
category,
deleteCategoryHandler
)
}
tabIndex={0}
>
<Icon icon="mdiDelete" />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.updateHandler(category)} onClick={() =>
tabIndex={0}> props.updateHandler(category)
<Icon icon='mdiPencil' /> }
tabIndex={0}
>
<Icon icon="mdiPencil" />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.pinCategory(category)} onClick={() => props.pinCategory(category)}
onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)} onKeyDown={(e) =>
tabIndex={0}> keyboardActionHandler(
{category.isPinned e,
? <Icon icon='mdiPinOff' color='var(--color-accent)' /> category,
: <Icon icon='mdiPin' /> props.pinCategory
)
} }
tabIndex={0}
>
{category.isPinned ? (
<Icon
icon="mdiPinOff"
color="var(--color-accent)"
/>
) : (
<Icon icon="mdiPin" />
)}
</div> </div>
</td> </td>
)} )}
</tr> </tr>
) );
}} }}
</Draggable> </Draggable>
) );
})} }
)}
</Table> </Table>
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
</Fragment> </Fragment>
) );
} else { } else {
const bookmarks: {bookmark: Bookmark, categoryName: string}[] = []; const bookmarks: { bookmark: Bookmark; categoryName: string }[] = [];
props.categories.forEach((category: Category) => { props.categories.forEach((category: Category) => {
category.bookmarks.forEach((bookmark: Bookmark) => { category.bookmarks.forEach((bookmark: Bookmark) => {
bookmarks.push({ bookmarks.push({
bookmark, bookmark,
categoryName: category.name categoryName: category.name,
});
});
}); });
})
})
return ( return (
<Table headers={[ <Table headers={['Name', 'URL', 'Icon', 'Category', 'Actions']}>
'Name', {bookmarks.map(
'URL', (bookmark: { bookmark: Bookmark; categoryName: string }) => {
'Icon',
'Category',
'Actions'
]}>
{bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => {
return ( return (
<tr key={bookmark.bookmark.id}> <tr key={bookmark.bookmark.id}>
<td>{bookmark.bookmark.name}</td> <td>{bookmark.bookmark.name}</td>
@ -196,30 +245,39 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => deleteBookmarkHandler(bookmark.bookmark)} onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
tabIndex={0}> tabIndex={0}
<Icon icon='mdiDelete' /> >
<Icon icon="mdiDelete" />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.updateHandler(bookmark.bookmark)} onClick={() => props.updateHandler(bookmark.bookmark)}
tabIndex={0}> tabIndex={0}
<Icon icon='mdiPencil' /> >
<Icon icon="mdiPencil" />
</div> </div>
</td> </td>
</tr> </tr>
) );
})} }
)}
</Table> </Table>
) );
}
} }
};
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
const actions = { const actions = {
pinCategory, pinCategory,
deleteCategory, deleteCategory,
deleteBookmark, deleteBookmark,
createNotification, createNotification,
reorderCategories reorderCategories,
} };
export default connect(null, actions)(BookmarkTable); export default connect(mapStateToProps, actions)(BookmarkTable);

View file

@ -7,7 +7,7 @@ import { getApps, getCategories } from '../../store/actions';
// Typescript // Typescript
import { GlobalState } from '../../interfaces/GlobalState'; import { GlobalState } from '../../interfaces/GlobalState';
import { App, Category } from '../../interfaces'; import { App, Category, Config } from '../../interfaces';
// UI // UI
import Icon from '../UI/Icons/Icon/Icon'; import Icon from '../UI/Icons/Icon/Icon';
@ -28,9 +28,6 @@ import SearchBar from '../SearchBar/SearchBar';
import { greeter } from './functions/greeter'; import { greeter } from './functions/greeter';
import { dateTime } from './functions/dateTime'; import { dateTime } from './functions/dateTime';
// Utils
import { searchConfig } from '../../utility';
interface ComponentProps { interface ComponentProps {
getApps: Function; getApps: Function;
getCategories: Function; getCategories: Function;
@ -38,6 +35,7 @@ interface ComponentProps {
apps: App[]; apps: App[];
categoriesLoading: boolean; categoriesLoading: boolean;
categories: Category[]; categories: Category[];
config: Config;
} }
const Home = (props: ComponentProps): JSX.Element => { const Home = (props: ComponentProps): JSX.Element => {
@ -77,7 +75,7 @@ const Home = (props: ComponentProps): JSX.Element => {
let interval: any; let interval: any;
// Start interval only when hideHeader is false // Start interval only when hideHeader is false
if (searchConfig('hideHeader', 0) !== 1) { if (!props.config.hideHeader) {
interval = setInterval(() => { interval = setInterval(() => {
setHeader({ setHeader({
dateTime: dateTime(), dateTime: dateTime(),
@ -103,13 +101,13 @@ const Home = (props: ComponentProps): JSX.Element => {
return ( return (
<Container> <Container>
{searchConfig('hideSearch', 0) !== 1 ? ( {!props.config.hideSearch ? (
<SearchBar setLocalSearch={setLocalSearch} /> <SearchBar setLocalSearch={setLocalSearch} />
) : ( ) : (
<div></div> <div></div>
)} )}
{searchConfig('hideHeader', 0) !== 1 ? ( {!props.config.hideHeader ? (
<header className={classes.Header}> <header className={classes.Header}>
<p>{header.dateTime}</p> <p>{header.dateTime}</p>
<Link to="/settings" className={classes.SettingsLink}> <Link to="/settings" className={classes.SettingsLink}>
@ -124,7 +122,7 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div> <div></div>
)} )}
{searchConfig('hideApps', 0) !== 1 ? ( {!props.config.hideApps ? (
<Fragment> <Fragment>
<SectionHeadline title="Applications" link="/applications" /> <SectionHeadline title="Applications" link="/applications" />
{appsLoading ? ( {appsLoading ? (
@ -148,7 +146,7 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div> <div></div>
)} )}
{searchConfig('hideCategories', 0) !== 1 ? ( {!props.config.hideCategories ? (
<Fragment> <Fragment>
<SectionHeadline title="Bookmarks" link="/bookmarks" /> <SectionHeadline title="Bookmarks" link="/bookmarks" />
{categoriesLoading ? ( {categoriesLoading ? (
@ -182,6 +180,7 @@ const mapStateToProps = (state: GlobalState) => {
apps: state.app.apps, apps: state.app.apps,
categoriesLoading: state.bookmark.loading, categoriesLoading: state.bookmark.loading,
categories: state.bookmark.categories, categories: state.bookmark.categories,
config: state.config.config,
}; };
}; };

View file

@ -1,8 +1,39 @@
export const dateTime = (): string => { export const dateTime = (): string => {
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const days = [
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const now = new Date(); const now = new Date();
return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`; const useAmericanDate = localStorage.useAmericanDate === 'true';
if (!useAmericanDate) {
return `${days[now.getDay()]}, ${now.getDate()} ${
months[now.getMonth()]
} ${now.getFullYear()}`;
} else {
return `${days[now.getDay()]}, ${
months[now.getMonth()]
} ${now.getDate()} ${now.getFullYear()}`;
} }
};

View file

@ -27,6 +27,11 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
inputRef.current.focus(); inputRef.current.focus();
}, []); }, []);
const clearSearch = () => {
inputRef.current.value = '';
setLocalSearch('');
};
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => { const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
const { isLocal, search, query, isURL, sameTab } = searchParser( const { isLocal, search, query, isURL, sameTab } = searchParser(
inputRef.current.value inputRef.current.value
@ -36,31 +41,40 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
setLocalSearch(search); setLocalSearch(search);
} }
if (e.code === 'Enter') { if (e.code === 'Enter' || e.code === 'NumpadEnter') {
if (!query.prefix) { if (!query.prefix) {
// Prefix not found -> emit notification
createNotification({ createNotification({
title: 'Error', title: 'Error',
message: 'Prefix not found', message: 'Prefix not found',
}); });
} else if (isURL) { } else if (isURL) {
// URL or IP passed -> redirect
const url = urlParser(inputRef.current.value)[1]; const url = urlParser(inputRef.current.value)[1];
redirectUrl(url, sameTab); redirectUrl(url, sameTab);
} else if (isLocal) { } else if (isLocal) {
// Local query -> filter apps and bookmarks
setLocalSearch(search); setLocalSearch(search);
} else { } else {
// Valid query -> redirect to search results
const url = `${query.template}${search}`; const url = `${query.template}${search}`;
redirectUrl(url, sameTab); redirectUrl(url, sameTab);
} }
} else if (e.code === 'Escape') {
clearSearch();
} }
}; };
return ( return (
<div className={classes.SearchContainer}>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
className={classes.SearchBar} className={classes.SearchBar}
onKeyUp={(e) => searchHandler(e)} onKeyUp={(e) => searchHandler(e)}
onDoubleClick={clearSearch}
/> />
</div>
); );
}; };

View file

@ -11,9 +11,10 @@ import {
// Typescript // Typescript
import { import {
Config,
GlobalState, GlobalState,
NewNotification, NewNotification,
SettingsForm, OtherSettingsForm,
} from '../../../interfaces'; } from '../../../interfaces';
// UI // UI
@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
// Utils // Utils
import { searchConfig } from '../../../utility'; import { otherSettingsTemplate, inputHandler } from '../../../utility';
interface ComponentProps { interface ComponentProps {
createNotification: (notification: NewNotification) => void; createNotification: (notification: NewNotification) => void;
updateConfig: (formData: SettingsForm) => void; updateConfig: (formData: OtherSettingsForm) => void;
sortApps: () => void; sortApps: () => void;
sortCategories: () => void; sortCategories: () => void;
loading: boolean; loading: boolean;
config: Config;
} }
const OtherSettings = (props: ComponentProps): JSX.Element => { const OtherSettings = (props: ComponentProps): JSX.Element => {
const { config } = props;
// Initial state // Initial state
const [formData, setFormData] = useState<SettingsForm>({ const [formData, setFormData] = useState<OtherSettingsForm>(
customTitle: document.title, otherSettingsTemplate
pinAppsByDefault: 1, );
pinCategoriesByDefault: 1,
hideHeader: 0,
hideApps: 0,
hideCategories: 0,
useOrdering: 'createdAt',
appsSameTab: 0,
bookmarksSameTab: 0,
dockerApps: 1,
dockerHost: 'localhost',
kubernetesApps: 1,
unpinStoppedApps: 1,
});
// Get config // Get config
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
customTitle: searchConfig('customTitle', 'Flame'), ...config,
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
hideHeader: searchConfig('hideHeader', 0),
hideApps: searchConfig('hideApps', 0),
hideCategories: searchConfig('hideCategories', 0),
useOrdering: searchConfig('useOrdering', 'createdAt'),
appsSameTab: searchConfig('appsSameTab', 0),
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
dockerApps: searchConfig('dockerApps', 0),
dockerHost: searchConfig('dockerHost', 'localhost'),
kubernetesApps: searchConfig('kubernetesApps', 0),
unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
}); });
}, [props.loading]); }, [props.loading]);
@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
// Input handler // Input handler
const inputChangeHandler = ( const inputChangeHandler = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
isNumber?: boolean options?: { isNumber?: boolean; isBool?: boolean }
) => { ) => {
let value: string | number = e.target.value; inputHandler<OtherSettingsForm>({
e,
if (isNumber) { options,
value = parseFloat(value); setStateHandler: setFormData,
} state: formData,
setFormData({
...formData,
[e.target.name]: value,
}); });
}; };
@ -116,6 +92,18 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
<InputGroup>
<label htmlFor="useAmericanDate">Date formatting</label>
<select
id="useAmericanDate"
name="useAmericanDate"
value={formData.useAmericanDate ? 1 : 0}
onChange={(e) => inputChangeHandler(e, { isBool: true })}
>
<option value={1}>Friday, October 22 2021</option>
<option value={0}>Friday, 22 October 2021</option>
</select>
</InputGroup>
{/* BEAHVIOR OPTIONS */} {/* BEAHVIOR OPTIONS */}
<SettingsHeadline text="App Behavior" /> <SettingsHeadline text="App Behavior" />
@ -126,8 +114,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="pinAppsByDefault" id="pinAppsByDefault"
name="pinAppsByDefault" name="pinAppsByDefault"
value={formData.pinAppsByDefault} value={formData.pinAppsByDefault ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -140,8 +128,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="pinCategoriesByDefault" id="pinCategoriesByDefault"
name="pinCategoriesByDefault" name="pinCategoriesByDefault"
value={formData.pinCategoriesByDefault} value={formData.pinCategoriesByDefault ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -165,8 +153,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="appsSameTab" id="appsSameTab"
name="appsSameTab" name="appsSameTab"
value={formData.appsSameTab} value={formData.appsSameTab ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -177,8 +165,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="bookmarksSameTab" id="bookmarksSameTab"
name="bookmarksSameTab" name="bookmarksSameTab"
value={formData.bookmarksSameTab} value={formData.bookmarksSameTab ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -192,8 +180,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="hideHeader" id="hideHeader"
name="hideHeader" name="hideHeader"
value={formData.hideHeader} value={formData.hideHeader ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -204,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="hideApps" id="hideApps"
name="hideApps" name="hideApps"
value={formData.hideApps} value={formData.hideApps ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -216,8 +204,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="hideCategories" id="hideCategories"
name="hideCategories" name="hideCategories"
value={formData.hideCategories} value={formData.hideCategories ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -242,8 +230,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="dockerApps" id="dockerApps"
name="dockerApps" name="dockerApps"
value={formData.dockerApps} value={formData.dockerApps ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -256,8 +244,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="unpinStoppedApps" id="unpinStoppedApps"
name="unpinStoppedApps" name="unpinStoppedApps"
value={formData.unpinStoppedApps} value={formData.unpinStoppedApps ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -271,8 +259,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<select <select
id="kubernetesApps" id="kubernetesApps"
name="kubernetesApps" name="kubernetesApps"
value={formData.kubernetesApps} value={formData.kubernetesApps ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -286,6 +274,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
loading: state.config.loading, loading: state.config.loading,
config: state.config.config,
}; };
}; };

View file

@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css';
import Modal from '../../../UI/Modal/Modal'; import Modal from '../../../UI/Modal/Modal';
import Icon from '../../../UI/Icons/Icon/Icon'; import Icon from '../../../UI/Icons/Icon/Icon';
import { GlobalState, NewNotification, Query } from '../../../../interfaces'; import {
Config,
GlobalState,
NewNotification,
Query,
} from '../../../../interfaces';
import QueriesForm from './QueriesForm'; import QueriesForm from './QueriesForm';
import { deleteQuery, createNotification } from '../../../../store/actions'; import { deleteQuery, createNotification } from '../../../../store/actions';
import Button from '../../../UI/Buttons/Button/Button'; import Button from '../../../UI/Buttons/Button/Button';
import { searchConfig } from '../../../../utility';
interface Props { interface Props {
customQueries: Query[]; customQueries: Query[];
deleteQuery: (prefix: string) => {}; deleteQuery: (prefix: string) => {};
createNotification: (notification: NewNotification) => void; createNotification: (notification: NewNotification) => void;
config: Config;
} }
const CustomQueries = (props: Props): JSX.Element => { const CustomQueries = (props: Props): JSX.Element => {
@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => {
}; };
const deleteHandler = (query: Query) => { const deleteHandler = (query: Query) => {
const currentProvider = searchConfig('defaultSearchProvider', 'l'); const currentProvider = props.config.defaultSearchProvider;
const isCurrent = currentProvider === query.prefix; const isCurrent = currentProvider === query.prefix;
if (isCurrent) { if (isCurrent) {
@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => {
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
customQueries: state.config.customQueries, customQueries: state.config.customQueries,
config: state.config.config,
}; };
}; };

View file

@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions';
// Typescript // Typescript
import { import {
Config,
GlobalState, GlobalState,
NewNotification, NewNotification,
Query, Query,
@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
// Utils // Utils
import { searchConfig } from '../../../utility'; import { inputHandler, searchSettingsTemplate } from '../../../utility';
// Data // Data
import { queries } from '../../../utility/searchQueries.json'; import { queries } from '../../../utility/searchQueries.json';
@ -32,22 +33,17 @@ interface Props {
updateConfig: (formData: SearchForm) => void; updateConfig: (formData: SearchForm) => void;
loading: boolean; loading: boolean;
customQueries: Query[]; customQueries: Query[];
config: Config;
} }
const SearchSettings = (props: Props): JSX.Element => { const SearchSettings = (props: Props): JSX.Element => {
// Initial state // Initial state
const [formData, setFormData] = useState<SearchForm>({ const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
hideSearch: 0,
defaultSearchProvider: 'l',
searchSameTab: 0,
});
// Get config // Get config
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
hideSearch: searchConfig('hideSearch', 0), ...props.config,
defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'),
searchSameTab: searchConfig('searchSameTab', 0),
}); });
}, [props.loading]); }, [props.loading]);
@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => {
// Input handler // Input handler
const inputChangeHandler = ( const inputChangeHandler = (
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
isNumber?: boolean options?: { isNumber?: boolean; isBool?: boolean }
) => { ) => {
let value: string | number = e.target.value; inputHandler<SearchForm>({
e,
if (isNumber) { options,
value = parseFloat(value); setStateHandler: setFormData,
} state: formData,
setFormData({
...formData,
[e.target.name]: value,
}); });
}; };
@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => {
<select <select
id="searchSameTab" id="searchSameTab"
name="searchSameTab" name="searchSameTab"
value={formData.searchSameTab} value={formData.searchSameTab ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -122,8 +114,8 @@ const SearchSettings = (props: Props): JSX.Element => {
<select <select
id="hideSearch" id="hideSearch"
name="hideSearch" name="hideSearch"
value={formData.hideSearch} value={formData.hideSearch ? 1 : 0}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
> >
<option value={1}>True</option> <option value={1}>True</option>
<option value={0}>False</option> <option value={0}>False</option>
@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => {
return { return {
loading: state.config.loading, loading: state.config.loading,
customQueries: state.config.customQueries, customQueries: state.config.customQueries,
config: state.config.config,
}; };
}; };

View file

@ -6,38 +6,40 @@ import { connect } from 'react-redux';
import { createNotification, updateConfig } from '../../../store/actions'; import { createNotification, updateConfig } from '../../../store/actions';
// Typescript // Typescript
import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces'; import {
ApiResponse,
Config,
GlobalState,
NewNotification,
Weather,
WeatherForm,
} from '../../../interfaces';
// UI // UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button'; import Button from '../../UI/Buttons/Button/Button';
// Utils // Utils
import { searchConfig } from '../../../utility'; import { inputHandler, weatherSettingsTemplate } from '../../../utility';
interface ComponentProps { interface ComponentProps {
createNotification: (notification: NewNotification) => void; createNotification: (notification: NewNotification) => void;
updateConfig: (formData: WeatherForm) => void; updateConfig: (formData: WeatherForm) => void;
loading: boolean; loading: boolean;
config: Config;
} }
const WeatherSettings = (props: ComponentProps): JSX.Element => { const WeatherSettings = (props: ComponentProps): JSX.Element => {
// Initial state // Initial state
const [formData, setFormData] = useState<WeatherForm>({ const [formData, setFormData] = useState<WeatherForm>(
WEATHER_API_KEY: '', weatherSettingsTemplate
lat: 0, );
long: 0,
isCelsius: 1
})
// Get config // Get config
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''), ...props.config,
lat: searchConfig('lat', 0), });
long: searchConfig('long', 0),
isCelsius: searchConfig('isCelsius', 1)
})
}, [props.loading]); }, [props.loading]);
// Form handler // Form handler
@ -48,106 +50,107 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) { if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
props.createNotification({ props.createNotification({
title: 'Warning', title: 'Warning',
message: 'API key is missing. Weather Module will NOT work' message: 'API key is missing. Weather Module will NOT work',
}) });
} }
// Save settings // Save settings
await props.updateConfig(formData); await props.updateConfig(formData);
// Update weather // Update weather
axios.get<ApiResponse<Weather>>('/api/weather/update') axios
.get<ApiResponse<Weather>>('/api/weather/update')
.then(() => { .then(() => {
props.createNotification({ props.createNotification({
title: 'Success', title: 'Success',
message: 'Weather updated' message: 'Weather updated',
}) });
}) })
.catch((err) => { .catch((err) => {
props.createNotification({ props.createNotification({
title: 'Error', title: 'Error',
message: err.response.data.error message: err.response.data.error,
})
}); });
} });
};
// Input handler // Input handler
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => { const inputChangeHandler = (
let value: string | number = e.target.value; e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
options?: { isNumber?: boolean; isBool?: boolean }
if (isNumber) { ) => {
value = parseFloat(value); inputHandler<WeatherForm>({
} e,
options,
setFormData({ setStateHandler: setFormData,
...formData, state: formData,
[e.target.name]: value });
}) };
}
return ( return (
<form onSubmit={(e) => formSubmitHandler(e)}> <form onSubmit={(e) => formSubmitHandler(e)}>
<InputGroup> <InputGroup>
<label htmlFor='WEATHER_API_KEY'>API key</label> <label htmlFor="WEATHER_API_KEY">API key</label>
<input <input
type='text' type="text"
id='WEATHER_API_KEY' id="WEATHER_API_KEY"
name='WEATHER_API_KEY' name="WEATHER_API_KEY"
placeholder='secret' placeholder="secret"
value={formData.WEATHER_API_KEY} value={formData.WEATHER_API_KEY}
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
<span> <span>
Using Using
<a <a href="https://www.weatherapi.com/pricing.aspx" target="blank">
href='https://www.weatherapi.com/pricing.aspx' {' '}
target='blank'> Weather API
{' '}Weather API
</a> </a>
. Key is required for weather module to work. . Key is required for weather module to work.
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='lat'>Location latitude</label> <label htmlFor="lat">Location latitude</label>
<input <input
type='number' type="number"
id='lat' id="lat"
name='lat' name="lat"
placeholder='52.22' placeholder="52.22"
value={formData.lat} value={formData.lat}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isNumber: true })}
step='any' step="any"
lang='en-150' lang="en-150"
/> />
<span> <span>
You can use You can use
<a <a
href='https://www.latlong.net/convert-address-to-lat-long.html' href="https://www.latlong.net/convert-address-to-lat-long.html"
target='blank'> target="blank"
{' '}latlong.net >
{' '}
latlong.net
</a> </a>
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='long'>Location longitude</label> <label htmlFor="long">Location longitude</label>
<input <input
type='number' type="number"
id='long' id="long"
name='long' name="long"
placeholder='21.01' placeholder="21.01"
value={formData.long} value={formData.long}
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isNumber: true })}
step='any' step="any"
lang='en-150' lang="en-150"
/> />
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='isCelsius'>Temperature unit</label> <label htmlFor="isCelsius">Temperature unit</label>
<select <select
id='isCelsius' id="isCelsius"
name='isCelsius' name="isCelsius"
onChange={(e) => inputChangeHandler(e, true)} onChange={(e) => inputChangeHandler(e, { isBool: true })}
value={formData.isCelsius} value={formData.isCelsius ? 1 : 0}
> >
<option value={1}>Celsius</option> <option value={1}>Celsius</option>
<option value={0}>Fahrenheit</option> <option value={0}>Fahrenheit</option>
@ -155,13 +158,16 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
<Button>Save changes</Button> <Button>Save changes</Button>
</form> </form>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
loading: state.config.loading loading: state.config.loading,
} config: state.config.config,
} };
};
export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings); export default connect(mapStateToProps, { createNotification, updateConfig })(
WeatherSettings
);

View file

@ -5,7 +5,7 @@ import axios from 'axios';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
// Typescript // Typescript
import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces'; import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces';
// CSS // CSS
import classes from './WeatherWidget.module.css'; import classes from './WeatherWidget.module.css';
@ -13,12 +13,9 @@ import classes from './WeatherWidget.module.css';
// UI // UI
import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon'; import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
// Utils
import { searchConfig } from '../../../utility';
interface ComponentProps { interface ComponentProps {
configLoading: boolean; configLoading: boolean;
config: Config[]; config: Config;
} }
const WeatherWidget = (props: ComponentProps): JSX.Element => { const WeatherWidget = (props: ComponentProps): JSX.Element => {
@ -32,26 +29,28 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
conditionCode: 1000, conditionCode: 1000,
id: -1, id: -1,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Initial request to get data // Initial request to get data
useEffect(() => { useEffect(() => {
axios.get<ApiResponse<Weather[]>>('/api/weather') axios
.then(data => { .get<ApiResponse<Weather[]>>('/api/weather')
.then((data) => {
const weatherData = data.data.data[0]; const weatherData = data.data.data[0];
if (weatherData) { if (weatherData) {
setWeather(weatherData); setWeather(weatherData);
} }
setIsLoading(false); setIsLoading(false);
}) })
.catch(err => console.log(err)); .catch((err) => console.log(err));
}, []); }, []);
// Open socket for data updates // Open socket for data updates
useEffect(() => { useEffect(() => {
const socketProtocol = document.location.protocol === 'http:' ? 'ws:' : 'wss:'; const socketProtocol =
document.location.protocol === 'http:' ? 'ws:' : 'wss:';
const socketAddress = `${socketProtocol}//${window.location.host}/socket`; const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
const webSocketClient = new WebSocket(socketAddress); const webSocketClient = new WebSocket(socketAddress);
@ -59,18 +58,19 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
setWeather({ setWeather({
...weather, ...weather,
...data ...data,
}) });
} };
return () => webSocketClient.close(); return () => webSocketClient.close();
}, []); }, []);
return ( return (
<div className={classes.WeatherWidget}> <div className={classes.WeatherWidget}>
{(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) && {isLoading ||
(weather.id > 0 && props.configLoading ||
(<Fragment> (props.config.WEATHER_API_KEY && weather.id > 0 && (
<Fragment>
<div className={classes.WeatherIcon}> <div className={classes.WeatherIcon}>
<WeatherIcon <WeatherIcon
weatherStatusCode={weather.conditionCode} weatherStatusCode={weather.conditionCode}
@ -78,24 +78,24 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
/> />
</div> </div>
<div className={classes.WeatherDetails}> <div className={classes.WeatherDetails}>
{searchConfig('isCelsius', true) {props.config.isCelsius ? (
? <span>{weather.tempC}°C</span> <span>{weather.tempC}°C</span>
: <span>{weather.tempF}°F</span> ) : (
} <span>{weather.tempF}°F</span>
)}
<span>{weather.cloud}%</span> <span>{weather.cloud}%</span>
</div> </div>
</Fragment>) </Fragment>
) ))}
}
</div> </div>
) );
} };
const mapStateToProps = (state: GlobalState) => { const mapStateToProps = (state: GlobalState) => {
return { return {
configLoading: state.config.loading, configLoading: state.config.loading,
config: state.config.config config: state.config.config,
} };
} };
export default connect(mapStateToProps)(WeatherWidget); export default connect(mapStateToProps)(WeatherWidget);

View file

@ -1,8 +1,23 @@
import { Model } from './'; export interface Config {
WEATHER_API_KEY: string;
export interface Config extends Model { lat: number;
key: string; long: number;
value: string; isCelsius: boolean;
valueType: string; customTitle: string;
isLocked: boolean; pinAppsByDefault: boolean;
pinCategoriesByDefault: boolean;
hideHeader: boolean;
useOrdering: string;
appsSameTab: boolean;
bookmarksSameTab: boolean;
searchSameTab: boolean;
hideApps: boolean;
hideCategories: boolean;
hideSearch: boolean;
defaultSearchProvider: string;
dockerApps: boolean;
dockerHost: string;
kubernetesApps: boolean;
unpinStoppedApps: boolean;
useAmericanDate: boolean;
} }

View file

@ -2,30 +2,28 @@ export interface WeatherForm {
WEATHER_API_KEY: string; WEATHER_API_KEY: string;
lat: number; lat: number;
long: number; long: number;
isCelsius: number; isCelsius: boolean;
} }
export interface SearchForm { export interface SearchForm {
hideSearch: number; hideSearch: boolean;
defaultSearchProvider: string; defaultSearchProvider: string;
searchSameTab: number; searchSameTab: boolean;
} }
export interface SettingsForm { export interface OtherSettingsForm {
customTitle: string; customTitle: string;
pinAppsByDefault: number; pinAppsByDefault: boolean;
pinCategoriesByDefault: number; pinCategoriesByDefault: boolean;
hideHeader: number; hideHeader: boolean;
hideApps: number; hideApps: boolean;
hideCategories: number; hideCategories: boolean;
// hideSearch: number;
// defaultSearchProvider: string;
useOrdering: string; useOrdering: string;
appsSameTab: number; appsSameTab: boolean;
bookmarksSameTab: number; bookmarksSameTab: boolean;
// searchSameTab: number; dockerApps: boolean;
dockerApps: number;
dockerHost: string; dockerHost: string;
kubernetesApps: number; kubernetesApps: boolean;
unpinStoppedApps: number; unpinStoppedApps: boolean;
useAmericanDate: boolean;
} }

View file

@ -5,14 +5,17 @@ import { App, ApiResponse, NewApp, Config } from '../../interfaces';
import { CreateNotificationAction } from './notification'; import { CreateNotificationAction } from './notification';
export interface GetAppsAction<T> { export interface GetAppsAction<T> {
type: ActionTypes.getApps | ActionTypes.getAppsSuccess | ActionTypes.getAppsError; type:
| ActionTypes.getApps
| ActionTypes.getAppsSuccess
| ActionTypes.getAppsError;
payload: T; payload: T;
} }
export const getApps = () => async (dispatch: Dispatch) => { export const getApps = () => async (dispatch: Dispatch) => {
dispatch<GetAppsAction<undefined>>({ dispatch<GetAppsAction<undefined>>({
type: ActionTypes.getApps, type: ActionTypes.getApps,
payload: undefined payload: undefined,
}); });
try { try {
@ -20,12 +23,12 @@ export const getApps = () => async (dispatch: Dispatch) => {
dispatch<GetAppsAction<App[]>>({ dispatch<GetAppsAction<App[]>>({
type: ActionTypes.getAppsSuccess, type: ActionTypes.getAppsSuccess,
payload: res.data.data payload: res.data.data,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface PinAppAction { export interface PinAppAction {
type: ActionTypes.pinApp; type: ActionTypes.pinApp;
@ -35,33 +38,38 @@ export interface PinAppAction {
export const pinApp = (app: App) => async (dispatch: Dispatch) => { export const pinApp = (app: App) => async (dispatch: Dispatch) => {
try { try {
const { id, isPinned, name } = app; const { id, isPinned, name } = app;
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, { isPinned: !isPinned }); const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, {
isPinned: !isPinned,
});
const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen'; const status = isPinned
? 'unpinned from Homescreen'
: 'pinned to Homescreen';
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `App ${name} ${status}` message: `App ${name} ${status}`,
} },
}) });
dispatch<PinAppAction>({ dispatch<PinAppAction>({
type: ActionTypes.pinApp, type: ActionTypes.pinApp,
payload: res.data.data payload: res.data.data,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface AddAppAction { export interface AddAppAction {
type: ActionTypes.addAppSuccess; type: ActionTypes.addAppSuccess;
payload: App; payload: App;
} }
export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => { export const addApp =
(formData: NewApp | FormData) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.post<ApiResponse<App>>('/api/apps', formData); const res = await axios.post<ApiResponse<App>>('/api/apps', formData);
@ -69,25 +77,25 @@ export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `App added` message: `App added`,
} },
}) });
await dispatch<AddAppAction>({ await dispatch<AddAppAction>({
type: ActionTypes.addAppSuccess, type: ActionTypes.addAppSuccess,
payload: res.data.data payload: res.data.data,
}) });
// Sort apps // Sort apps
dispatch<any>(sortApps()) dispatch<any>(sortApps());
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface DeleteAppAction { export interface DeleteAppAction {
type: ActionTypes.deleteApp, type: ActionTypes.deleteApp;
payload: number payload: number;
} }
export const deleteApp = (id: number) => async (dispatch: Dispatch) => { export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
@ -98,79 +106,85 @@ export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: 'App deleted' message: 'App deleted',
} },
}) });
dispatch<DeleteAppAction>({ dispatch<DeleteAppAction>({
type: ActionTypes.deleteApp, type: ActionTypes.deleteApp,
payload: id payload: id,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface UpdateAppAction { export interface UpdateAppAction {
type: ActionTypes.updateApp; type: ActionTypes.updateApp;
payload: App; payload: App;
} }
export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { export const updateApp =
(id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.put<ApiResponse<App>>(`/api/apps/${id}`, formData); const res = await axios.put<ApiResponse<App>>(
`/api/apps/${id}`,
formData
);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `App updated` message: `App updated`,
} },
}) });
await dispatch<UpdateAppAction>({ await dispatch<UpdateAppAction>({
type: ActionTypes.updateApp, type: ActionTypes.updateApp,
payload: res.data.data payload: res.data.data,
}) });
// Sort apps // Sort apps
dispatch<any>(sortApps()) dispatch<any>(sortApps());
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface ReorderAppsAction { export interface ReorderAppsAction {
type: ActionTypes.reorderApps; type: ActionTypes.reorderApps;
payload: App[] payload: App[];
} }
interface ReorderQuery { interface ReorderQuery {
apps: { apps: {
id: number; id: number;
orderId: number; orderId: number;
}[] }[];
} }
export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => { export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
try { try {
const updateQuery: ReorderQuery = { apps: [] } const updateQuery: ReorderQuery = { apps: [] };
apps.forEach((app, index) => updateQuery.apps.push({ apps.forEach((app, index) =>
updateQuery.apps.push({
id: app.id, id: app.id,
orderId: index + 1 orderId: index + 1,
})) })
);
await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery); await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
dispatch<ReorderAppsAction>({ dispatch<ReorderAppsAction>({
type: ActionTypes.reorderApps, type: ActionTypes.reorderApps,
payload: apps payload: apps,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
export interface SortAppsAction { export interface SortAppsAction {
type: ActionTypes.sortApps; type: ActionTypes.sortApps;
@ -179,13 +193,13 @@ export interface SortAppsAction {
export const sortApps = () => async (dispatch: Dispatch) => { export const sortApps = () => async (dispatch: Dispatch) => {
try { try {
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering'); const res = await axios.get<ApiResponse<Config>>('/api/config');
dispatch<SortAppsAction>({ dispatch<SortAppsAction>({
type: ActionTypes.sortApps, type: ActionTypes.sortApps,
payload: res.data.data.value payload: res.data.data.useOrdering,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };

View file

@ -1,133 +1,157 @@
import axios from 'axios'; import axios from 'axios';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { ActionTypes } from './actionTypes'; import { ActionTypes } from './actionTypes';
import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark, Config } from '../../interfaces'; import {
Category,
ApiResponse,
NewCategory,
Bookmark,
NewBookmark,
Config,
} from '../../interfaces';
import { CreateNotificationAction } from './notification'; import { CreateNotificationAction } from './notification';
/** /**
* GET CATEGORIES * GET CATEGORIES
*/ */
export interface GetCategoriesAction<T> { export interface GetCategoriesAction<T> {
type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError; type:
| ActionTypes.getCategories
| ActionTypes.getCategoriesSuccess
| ActionTypes.getCategoriesError;
payload: T; payload: T;
} }
export const getCategories = () => async (dispatch: Dispatch) => { export const getCategories = () => async (dispatch: Dispatch) => {
dispatch<GetCategoriesAction<undefined>>({ dispatch<GetCategoriesAction<undefined>>({
type: ActionTypes.getCategories, type: ActionTypes.getCategories,
payload: undefined payload: undefined,
}) });
try { try {
const res = await axios.get<ApiResponse<Category[]>>('/api/categories'); const res = await axios.get<ApiResponse<Category[]>>('/api/categories');
dispatch<GetCategoriesAction<Category[]>>({ dispatch<GetCategoriesAction<Category[]>>({
type: ActionTypes.getCategoriesSuccess, type: ActionTypes.getCategoriesSuccess,
payload: res.data.data payload: res.data.data,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* ADD CATEGORY * ADD CATEGORY
*/ */
export interface AddCategoryAction { export interface AddCategoryAction {
type: ActionTypes.addCategory, type: ActionTypes.addCategory;
payload: Category payload: Category;
} }
export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) => { export const addCategory =
(formData: NewCategory) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.post<ApiResponse<Category>>('/api/categories', formData); const res = await axios.post<ApiResponse<Category>>(
'/api/categories',
formData
);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Category ${formData.name} created` message: `Category ${formData.name} created`,
} },
}) });
dispatch<AddCategoryAction>({ dispatch<AddCategoryAction>({
type: ActionTypes.addCategory, type: ActionTypes.addCategory,
payload: res.data.data payload: res.data.data,
}) });
dispatch<any>(sortCategories()); dispatch<any>(sortCategories());
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* ADD BOOKMARK * ADD BOOKMARK
*/ */
export interface AddBookmarkAction { export interface AddBookmarkAction {
type: ActionTypes.addBookmark, type: ActionTypes.addBookmark;
payload: Bookmark payload: Bookmark;
} }
export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { export const addBookmark =
(formData: NewBookmark | FormData) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.post<ApiResponse<Bookmark>>('/api/bookmarks', formData); const res = await axios.post<ApiResponse<Bookmark>>(
'/api/bookmarks',
formData
);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Bookmark created` message: `Bookmark created`,
} },
}) });
dispatch<AddBookmarkAction>({ dispatch<AddBookmarkAction>({
type: ActionTypes.addBookmark, type: ActionTypes.addBookmark,
payload: res.data.data payload: res.data.data,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* PIN CATEGORY * PIN CATEGORY
*/ */
export interface PinCategoryAction { export interface PinCategoryAction {
type: ActionTypes.pinCategory, type: ActionTypes.pinCategory;
payload: Category payload: Category;
} }
export const pinCategory = (category: Category) => async (dispatch: Dispatch) => { export const pinCategory =
(category: Category) => async (dispatch: Dispatch) => {
try { try {
const { id, isPinned, name } = category; const { id, isPinned, name } = category;
const res = await axios.put<ApiResponse<Category>>(`/api/categories/${id}`, { isPinned: !isPinned }); const res = await axios.put<ApiResponse<Category>>(
`/api/categories/${id}`,
{ isPinned: !isPinned }
);
const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen'; const status = isPinned
? 'unpinned from Homescreen'
: 'pinned to Homescreen';
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Category ${name} ${status}` message: `Category ${name} ${status}`,
} },
}) });
dispatch<PinCategoryAction>({ dispatch<PinCategoryAction>({
type: ActionTypes.pinCategory, type: ActionTypes.pinCategory,
payload: res.data.data payload: res.data.data,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* DELETE CATEGORY * DELETE CATEGORY
*/ */
export interface DeleteCategoryAction { export interface DeleteCategoryAction {
type: ActionTypes.deleteCategory, type: ActionTypes.deleteCategory;
payload: number payload: number;
} }
export const deleteCategory = (id: number) => async (dispatch: Dispatch) => { export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
@ -138,62 +162,67 @@ export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Category deleted` message: `Category deleted`,
} },
}) });
dispatch<DeleteCategoryAction>({ dispatch<DeleteCategoryAction>({
type: ActionTypes.deleteCategory, type: ActionTypes.deleteCategory,
payload: id payload: id,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* UPDATE CATEGORY * UPDATE CATEGORY
*/ */
export interface UpdateCategoryAction { export interface UpdateCategoryAction {
type: ActionTypes.updateCategory, type: ActionTypes.updateCategory;
payload: Category payload: Category;
} }
export const updateCategory = (id: number, formData: NewCategory) => async (dispatch: Dispatch) => { export const updateCategory =
(id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.put<ApiResponse<Category>>(`/api/categories/${id}`, formData); const res = await axios.put<ApiResponse<Category>>(
`/api/categories/${id}`,
formData
);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Category ${formData.name} updated` message: `Category ${formData.name} updated`,
} },
}) });
dispatch<UpdateCategoryAction>({ dispatch<UpdateCategoryAction>({
type: ActionTypes.updateCategory, type: ActionTypes.updateCategory,
payload: res.data.data payload: res.data.data,
}) });
dispatch<any>(sortCategories()); dispatch<any>(sortCategories());
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* DELETE BOOKMARK * DELETE BOOKMARK
*/ */
export interface DeleteBookmarkAction { export interface DeleteBookmarkAction {
type: ActionTypes.deleteBookmark, type: ActionTypes.deleteBookmark;
payload: { payload: {
bookmarkId: number, bookmarkId: number;
categoryId: number categoryId: number;
} };
} }
export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => { export const deleteBookmark =
(bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => {
try { try {
await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`); await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`);
@ -201,48 +230,53 @@ export const deleteBookmark = (bookmarkId: number, categoryId: number) => async
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: 'Bookmark deleted' message: 'Bookmark deleted',
} },
}) });
dispatch<DeleteBookmarkAction>({ dispatch<DeleteBookmarkAction>({
type: ActionTypes.deleteBookmark, type: ActionTypes.deleteBookmark,
payload: { payload: {
bookmarkId, bookmarkId,
categoryId categoryId,
} },
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* UPDATE BOOKMARK * UPDATE BOOKMARK
*/ */
export interface UpdateBookmarkAction { export interface UpdateBookmarkAction {
type: ActionTypes.updateBookmark, type: ActionTypes.updateBookmark;
payload: Bookmark payload: Bookmark;
} }
export const updateBookmark = ( export const updateBookmark =
(
bookmarkId: number, bookmarkId: number,
formData: NewBookmark | FormData, formData: NewBookmark | FormData,
category: { category: {
prev: number, prev: number;
curr: number curr: number;
} }
) => async (dispatch: Dispatch) => { ) =>
async (dispatch: Dispatch) => {
try { try {
const res = await axios.put<ApiResponse<Bookmark>>(`/api/bookmarks/${bookmarkId}`, formData); const res = await axios.put<ApiResponse<Bookmark>>(
`/api/bookmarks/${bookmarkId}`,
formData
);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
payload: { payload: {
title: 'Success', title: 'Success',
message: `Bookmark updated` message: `Bookmark updated`,
} },
}) });
// Check if category was changed // Check if category was changed
const categoryWasChanged = category.curr !== category.prev; const categoryWasChanged = category.curr !== category.prev;
@ -253,26 +287,26 @@ export const updateBookmark = (
type: ActionTypes.deleteBookmark, type: ActionTypes.deleteBookmark,
payload: { payload: {
bookmarkId, bookmarkId,
categoryId: category.prev categoryId: category.prev,
} },
}) });
// Add bookmark to the new category // Add bookmark to the new category
dispatch<AddBookmarkAction>({ dispatch<AddBookmarkAction>({
type: ActionTypes.addBookmark, type: ActionTypes.addBookmark,
payload: res.data.data payload: res.data.data,
}) });
} else { } else {
// Else update only name/url/icon // Else update only name/url/icon
dispatch<UpdateBookmarkAction>({ dispatch<UpdateBookmarkAction>({
type: ActionTypes.updateBookmark, type: ActionTypes.updateBookmark,
payload: res.data.data payload: res.data.data,
}) });
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* SORT CATEGORIES * SORT CATEGORIES
@ -284,16 +318,16 @@ export interface SortCategoriesAction {
export const sortCategories = () => async (dispatch: Dispatch) => { export const sortCategories = () => async (dispatch: Dispatch) => {
try { try {
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering'); const res = await axios.get<ApiResponse<Config>>('/api/config');
dispatch<SortCategoriesAction>({ dispatch<SortCategoriesAction>({
type: ActionTypes.sortCategories, type: ActionTypes.sortCategories,
payload: res.data.data.value payload: res.data.data.useOrdering,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };
/** /**
* REORDER CATEGORIES * REORDER CATEGORIES
@ -307,25 +341,31 @@ interface ReorderQuery {
categories: { categories: {
id: number; id: number;
orderId: number; orderId: number;
}[] }[];
} }
export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => { export const reorderCategories =
(categories: Category[]) => async (dispatch: Dispatch) => {
try { try {
const updateQuery: ReorderQuery = { categories: [] } const updateQuery: ReorderQuery = { categories: [] };
categories.forEach((category, index) => updateQuery.categories.push({ categories.forEach((category, index) =>
updateQuery.categories.push({
id: category.id, id: category.id,
orderId: index + 1 orderId: index + 1,
})) })
);
await axios.put<ApiResponse<{}>>('/api/categories/0/reorder', updateQuery); await axios.put<ApiResponse<{}>>(
'/api/categories/0/reorder',
updateQuery
);
dispatch<ReorderCategoriesAction>({ dispatch<ReorderCategoriesAction>({
type: ActionTypes.reorderCategories, type: ActionTypes.reorderCategories,
payload: categories payload: categories,
}) });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
} };

View file

@ -3,16 +3,15 @@ import { Dispatch } from 'redux';
import { ActionTypes } from './actionTypes'; import { ActionTypes } from './actionTypes';
import { Config, ApiResponse, Query } from '../../interfaces'; import { Config, ApiResponse, Query } from '../../interfaces';
import { CreateNotificationAction } from './notification'; import { CreateNotificationAction } from './notification';
import { searchConfig } from '../../utility';
export interface GetConfigAction { export interface GetConfigAction {
type: ActionTypes.getConfig; type: ActionTypes.getConfig;
payload: Config[]; payload: Config;
} }
export const getConfig = () => async (dispatch: Dispatch) => { export const getConfig = () => async (dispatch: Dispatch) => {
try { try {
const res = await axios.get<ApiResponse<Config[]>>('/api/config'); const res = await axios.get<ApiResponse<Config>>('/api/config');
dispatch<GetConfigAction>({ dispatch<GetConfigAction>({
type: ActionTypes.getConfig, type: ActionTypes.getConfig,
@ -20,7 +19,10 @@ export const getConfig = () => async (dispatch: Dispatch) => {
}); });
// Set custom page title if set // Set custom page title if set
document.title = searchConfig('customTitle', 'Flame'); document.title = res.data.data.customTitle;
// Store settings for priority UI elements
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@ -28,12 +30,12 @@ export const getConfig = () => async (dispatch: Dispatch) => {
export interface UpdateConfigAction { export interface UpdateConfigAction {
type: ActionTypes.updateConfig; type: ActionTypes.updateConfig;
payload: Config[]; payload: Config;
} }
export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
try { try {
const res = await axios.put<ApiResponse<Config[]>>('/api/config', formData); const res = await axios.put<ApiResponse<Config>>('/api/config', formData);
dispatch<CreateNotificationAction>({ dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification, type: ActionTypes.createNotification,
@ -47,6 +49,9 @@ export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
type: ActionTypes.updateConfig, type: ActionTypes.updateConfig,
payload: res.data.data, payload: res.data.data,
}); });
// Store settings for priority UI elements
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }

View file

@ -1,15 +1,16 @@
import { ActionTypes, Action } from '../actions'; import { ActionTypes, Action } from '../actions';
import { Config, Query } from '../../interfaces'; import { Config, Query } from '../../interfaces';
import { configTemplate } from '../../utility';
export interface State { export interface State {
loading: boolean; loading: boolean;
config: Config[]; config: Config;
customQueries: Query[]; customQueries: Query[];
} }
const initialState: State = { const initialState: State = {
loading: true, loading: true,
config: [], config: configTemplate,
customQueries: [], customQueries: [],
}; };

View file

@ -7,20 +7,22 @@ export interface State {
const initialState: State = { const initialState: State = {
theme: { theme: {
name: 'blues', name: 'tron',
colors: { colors: {
background: '#2B2C56', background: '#242B33',
primary: '#EFF1FC', primary: '#EFFBFF',
accent: '#6677EB' accent: '#6EE2FF',
} },
} },
} };
const themeReducer = (state = initialState, action: Action) => { const themeReducer = (state = initialState, action: Action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.setTheme: return { theme: action.payload }; case ActionTypes.setTheme:
default: return state; return { theme: action.payload };
} default:
return state;
} }
};
export default themeReducer; export default themeReducer;

View file

@ -1,7 +1,8 @@
export * from './iconParser'; export * from './iconParser';
export * from './urlParser'; export * from './urlParser';
export * from './searchConfig';
export * from './checkVersion'; export * from './checkVersion';
export * from './sortData'; export * from './sortData';
export * from './searchParser'; export * from './searchParser';
export * from './redirectUrl'; export * from './redirectUrl';
export * from './templateObjects';
export * from './inputHandler';

View file

@ -0,0 +1,39 @@
import { ChangeEvent, SetStateAction } from 'react';
type Event = ChangeEvent<HTMLInputElement | HTMLSelectElement>;
interface Options {
isNumber?: boolean;
isBool?: boolean;
}
interface Params<T> {
e: Event;
options?: Options;
setStateHandler: (v: SetStateAction<T>) => void;
state: T;
}
export const inputHandler = <T>(params: Params<T>): void => {
const { e, options, setStateHandler, state } = params;
const rawValue = e.target.value;
let value: string | number | boolean = e.target.value;
if (options) {
const { isNumber = false, isBool = false } = options;
if (isNumber) {
value = parseFloat(rawValue);
}
if (isBool) {
value = !!parseInt(rawValue);
}
}
setStateHandler({
...state,
[e.target.name]: value,
});
};

View file

@ -1,24 +0,0 @@
import { store } from '../store/store';
/**
* Search config store with given key
* @param key Config pair key to search
* @param _default Value to return if key is not found
*/
export const searchConfig = (key: string, _default: any) => {
const state = store.getState();
const pair = state.config.config.find(p => p.key === key);
if (pair) {
if (pair.valueType === 'number') {
return parseFloat(pair.value);
} else if (pair.valueType === 'boolean') {
return parseInt(pair.value);
} else {
return pair.value;
}
}
return _default;
}

View file

@ -1,7 +1,6 @@
import { queries } from './searchQueries.json'; import { queries } from './searchQueries.json';
import { Query, SearchResult } from '../interfaces'; import { Query, SearchResult } from '../interfaces';
import { store } from '../store/store'; import { store } from '../store/store';
import { searchConfig } from '.';
export const searchParser = (searchQuery: string): SearchResult => { export const searchParser = (searchQuery: string): SearchResult => {
const result: SearchResult = { const result: SearchResult = {
@ -16,20 +15,18 @@ export const searchParser = (searchQuery: string): SearchResult => {
}, },
}; };
const customQueries = store.getState().config.customQueries; const { customQueries, config } = store.getState().config;
// Check if url or ip was passed // Check if url or ip was passed
const urlRegex = const urlRegex =
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/i;
result.isURL = urlRegex.test(searchQuery); result.isURL = urlRegex.test(searchQuery);
// Match prefix and query // Match prefix and query
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i); const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
const prefix = splitQuery const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider;
? splitQuery[1]
: searchConfig('defaultSearchProvider', 'l');
const search = splitQuery const search = splitQuery
? encodeURIComponent(splitQuery[2]) ? encodeURIComponent(splitQuery[2])
@ -47,7 +44,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
if (prefix === 'l') { if (prefix === 'l') {
result.isLocal = true; result.isLocal = true;
} else { } else {
result.sameTab = searchConfig('searchSameTab', false); result.sameTab = config.searchSameTab;
} }
return result; return result;

View file

@ -0,0 +1,25 @@
import { Config } from '../../interfaces';
export const configTemplate: Config = {
WEATHER_API_KEY: '',
lat: 0,
long: 0,
isCelsius: true,
customTitle: 'Flame',
pinAppsByDefault: true,
pinCategoriesByDefault: true,
hideHeader: false,
useOrdering: 'createdAt',
appsSameTab: false,
bookmarksSameTab: false,
searchSameTab: false,
hideApps: false,
hideCategories: false,
hideSearch: false,
defaultSearchProvider: 'l',
dockerApps: false,
dockerHost: 'localhost',
kubernetesApps: false,
unpinStoppedApps: false,
useAmericanDate: false,
};

View file

@ -0,0 +1,2 @@
export * from './configTemplate';
export * from './settingsTemplate';

View file

@ -0,0 +1,31 @@
import { OtherSettingsForm, SearchForm, WeatherForm } from '../../interfaces';
export const otherSettingsTemplate: OtherSettingsForm = {
customTitle: document.title,
pinAppsByDefault: true,
pinCategoriesByDefault: true,
hideHeader: false,
hideApps: false,
hideCategories: false,
useOrdering: 'createdAt',
appsSameTab: false,
bookmarksSameTab: false,
dockerApps: true,
dockerHost: 'localhost',
kubernetesApps: true,
unpinStoppedApps: true,
useAmericanDate: false,
};
export const weatherSettingsTemplate: WeatherForm = {
WEATHER_API_KEY: '',
lat: 0,
long: 0,
isCelsius: true,
};
export const searchSettingsTemplate: SearchForm = {
hideSearch: false,
searchSameTab: false,
defaultSearchProvider: 'l',
};

View file

@ -1,326 +0,0 @@
const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const App = require('../models/App');
const Config = require('../models/Config');
const { Sequelize } = require('sequelize');
const axios = require('axios');
const Logger = require('../utils/Logger');
const logger = new Logger();
const k8s = require('@kubernetes/client-node');
// @desc Create new app
// @route POST /api/apps
// @access Public
exports.createApp = asyncWrapper(async (req, res, next) => {
// Get config from database
const pinApps = await Config.findOne({
where: { key: 'pinAppsByDefault' },
});
let app;
let _body = { ...req.body };
if (req.file) {
_body.icon = req.file.filename;
}
if (pinApps) {
if (parseInt(pinApps.value)) {
app = await App.create({
..._body,
isPinned: true,
});
} else {
app = await App.create(req.body);
}
}
res.status(201).json({
success: true,
data: app,
});
});
// @desc Get all apps
// @route GET /api/apps
// @access Public
exports.getApps = asyncWrapper(async (req, res, next) => {
// Get config from database
const useOrdering = await Config.findOne({
where: { key: 'useOrdering' },
});
const useDockerApi = await Config.findOne({
where: { key: 'dockerApps' },
});
const useKubernetesApi = await Config.findOne({
where: { key: 'kubernetesApps' },
});
const unpinStoppedApps = await Config.findOne({
where: { key: 'unpinStoppedApps' },
});
const orderType = useOrdering ? useOrdering.value : 'createdAt';
let apps;
if (useDockerApi && useDockerApi.value == 1) {
let containers = null;
const host = await Config.findOne({
where: { key: 'dockerHost' },
});
try {
if (host.value.includes('localhost')) {
let { data } = await axios.get(
`http://${host.value}/containers/json?{"status":["running"]}`,
{
socketPath: '/var/run/docker.sock',
}
);
containers = data;
} else {
let { data } = await axios.get(
`http://${host.value}/containers/json?{"status":["running"]}`
);
containers = data;
}
} catch {
logger.log(`Can't connect to the docker api on ${host.value}`, 'ERROR');
}
if (containers) {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
containers = containers.filter((e) => Object.keys(e.Labels).length !== 0);
const dockerApps = [];
for (const container of containers) {
const labels = container.Labels;
if (
'flame.name' in labels &&
'flame.url' in labels &&
/^app/.test(labels['flame.type'])
) {
for (let i = 0; i < labels['flame.name'].split(';').length; i++) {
const names = labels['flame.name'].split(';');
const urls = labels['flame.url'].split(';');
let icons = '';
if ('flame.icon' in labels) {
icons = labels['flame.icon'].split(';');
}
dockerApps.push({
name: names[i] || names[0],
url: urls[i] || urls[0],
icon: icons[i] || 'docker',
});
}
}
}
if (unpinStoppedApps && unpinStoppedApps.value == 1) {
for (const app of apps) {
await app.update({ isPinned: false });
}
}
for (const item of dockerApps) {
if (apps.some((app) => app.name === item.name)) {
const app = apps.filter((e) => e.name === item.name)[0];
if (
item.icon === 'custom' ||
(item.icon === 'docker' && app.icon != 'docker')
) {
await app.update({
name: item.name,
url: item.url,
isPinned: true,
});
} else {
await app.update({
name: item.name,
url: item.url,
icon: item.icon,
isPinned: true,
});
}
} else {
await App.create({
name: item.name,
url: item.url,
icon: item.icon === 'custom' ? 'docker' : item.icon,
isPinned: true,
});
}
}
}
}
if (useKubernetesApi && useKubernetesApi.value == 1) {
let ingresses = null;
try {
const kc = new k8s.KubeConfig();
kc.loadFromCluster();
const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api);
await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => {
ingresses = res.body.items;
});
} catch {
logger.log("Can't connect to the kubernetes api", 'ERROR');
}
if (ingresses) {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
ingresses = ingresses.filter(
(e) => Object.keys(e.metadata.annotations).length !== 0
);
const kubernetesApps = [];
for (const ingress of ingresses) {
const annotations = ingress.metadata.annotations;
if (
'flame.pawelmalak/name' in annotations &&
'flame.pawelmalak/url' in annotations &&
/^app/.test(annotations['flame.pawelmalak/type'])
) {
kubernetesApps.push({
name: annotations['flame.pawelmalak/name'],
url: annotations['flame.pawelmalak/url'],
icon: annotations['flame.pawelmalak/icon'] || 'kubernetes',
});
}
}
if (unpinStoppedApps && unpinStoppedApps.value == 1) {
for (const app of apps) {
await app.update({ isPinned: false });
}
}
for (const item of kubernetesApps) {
if (apps.some((app) => app.name === item.name)) {
const app = apps.filter((e) => e.name === item.name)[0];
await app.update({ ...item, isPinned: true });
} else {
await App.create({
...item,
isPinned: true,
});
}
}
}
}
if (orderType == 'name') {
apps = await App.findAll({
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
});
} else {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
}
if (process.env.NODE_ENV === 'production') {
// Set header to fetch containers info every time
res.status(200).setHeader('Cache-Control', 'no-store').json({
success: true,
data: apps,
});
return;
}
res.status(200).json({
success: true,
data: apps,
});
});
// @desc Get single app
// @route GET /api/apps/:id
// @access Public
exports.getApp = asyncWrapper(async (req, res, next) => {
const app = await App.findOne({
where: { id: req.params.id },
});
if (!app) {
return next(
new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)
);
}
res.status(200).json({
success: true,
data: app,
});
});
// @desc Update app
// @route PUT /api/apps/:id
// @access Public
exports.updateApp = asyncWrapper(async (req, res, next) => {
let app = await App.findOne({
where: { id: req.params.id },
});
if (!app) {
return next(
new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)
);
}
let _body = { ...req.body };
if (req.file) {
_body.icon = req.file.filename;
}
app = await app.update(_body);
res.status(200).json({
success: true,
data: app,
});
});
// @desc Delete app
// @route DELETE /api/apps/:id
// @access Public
exports.deleteApp = asyncWrapper(async (req, res, next) => {
await App.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});
// @desc Reorder apps
// @route PUT /api/apps/0/reorder
// @access Public
exports.reorderApps = asyncWrapper(async (req, res, next) => {
req.body.apps.forEach(async ({ id, orderId }) => {
await App.update(
{ orderId },
{
where: { id },
}
);
});
res.status(200).json({
success: true,
data: {},
});
});

View file

@ -0,0 +1,33 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
const loadConfig = require('../../utils/loadConfig');
// @desc Create new app
// @route POST /api/apps
// @access Public
const createApp = asyncWrapper(async (req, res, next) => {
const { pinAppsByDefault } = await loadConfig();
let app;
let _body = { ...req.body };
if (req.file) {
_body.icon = req.file.filename;
}
if (pinAppsByDefault) {
app = await App.create({
..._body,
isPinned: true,
});
} else {
app = await App.create(req.body);
}
res.status(201).json({
success: true,
data: app,
});
});
module.exports = createApp;

View file

@ -0,0 +1,18 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
// @desc Delete app
// @route DELETE /api/apps/:id
// @access Public
const deleteApp = asyncWrapper(async (req, res, next) => {
await App.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});
module.exports = deleteApp;

View file

@ -0,0 +1,4 @@
module.exports = {
useKubernetes: require('./useKubernetes'),
useDocker: require('./useDocker'),
};

View file

@ -0,0 +1,148 @@
const App = require('../../../models/App');
const axios = require('axios');
const Logger = require('../../../utils/Logger');
const logger = new Logger();
const loadConfig = require('../../../utils/loadConfig');
const useDocker = async (apps) => {
const {
useOrdering: orderType,
unpinStoppedApps,
dockerHost: host,
} = await loadConfig();
let containers = null;
// Get list of containers
try {
if (host.includes('localhost')) {
// Use default host
let { data } = await axios.get(
`http://${host}/containers/json?{"status":["running"]}`,
{
socketPath: '/var/run/docker.sock',
}
);
containers = data;
} else {
// Use custom host
let { data } = await axios.get(
`http://${host}/containers/json?{"status":["running"]}`
);
containers = data;
}
} catch {
logger.log(`Can't connect to the Docker API on ${host}`, 'ERROR');
}
if (containers) {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
// Filter out containers without any annotations
containers = containers.filter((e) => Object.keys(e.Labels).length !== 0);
const dockerApps = [];
for (const container of containers) {
let labels = container.Labels;
// Traefik labels for URL configuration
if (!('flame.url' in labels)) {
for (const label of Object.keys(labels)) {
if (/^traefik.*.frontend.rule/.test(label)) {
// Traefik 1.x
let value = labels[label];
if (value.indexOf('Host') !== -1) {
value = value.split('Host:')[1];
labels['flame.url'] =
'https://' + value.split(',').join(';https://');
}
} else if (/^traefik.*?\.rule/.test(label)) {
// Traefik 2.x
const value = labels[label];
if (value.indexOf('Host') !== -1) {
const regex = /\`([a-zA-Z0-9\.\-]+)\`/g;
const domains = [];
while ((match = regex.exec(value)) != null) {
domains.push('http://' + match[1]);
}
if (domains.length > 0) {
labels['flame.url'] = domains.join(';');
}
}
}
}
}
// add each container as flame formatted app
if (
'flame.name' in labels &&
'flame.url' in labels &&
/^app/.test(labels['flame.type'])
) {
for (let i = 0; i < labels['flame.name'].split(';').length; i++) {
const names = labels['flame.name'].split(';');
const urls = labels['flame.url'].split(';');
let icons = '';
if ('flame.icon' in labels) {
icons = labels['flame.icon'].split(';');
}
dockerApps.push({
name: names[i] || names[0],
url: urls[i] || urls[0],
icon: icons[i] || 'docker',
});
}
}
}
if (unpinStoppedApps) {
for (const app of apps) {
await app.update({ isPinned: false });
}
}
for (const item of dockerApps) {
// If app already exists, update it
if (apps.some((app) => app.name === item.name)) {
const app = apps.find((a) => a.name === item.name);
if (
item.icon === 'custom' ||
(item.icon === 'docker' && app.icon != 'docker')
) {
// update without overriding icon
await app.update({
name: item.name,
url: item.url,
isPinned: true,
});
} else {
await app.update({
...item,
isPinned: true,
});
}
} else {
// else create new app
await App.create({
...item,
icon: item.icon === 'custom' ? 'docker' : item.icon,
isPinned: true,
});
}
}
}
};
module.exports = useDocker;

View file

@ -0,0 +1,70 @@
const App = require('../../../models/App');
const k8s = require('@kubernetes/client-node');
const Logger = require('../../../utils/Logger');
const logger = new Logger();
const loadConfig = require('../../../utils/loadConfig');
const useKubernetes = async (apps) => {
const { useOrdering: orderType, unpinStoppedApps } = await loadConfig();
let ingresses = null;
try {
const kc = new k8s.KubeConfig();
kc.loadFromCluster();
const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api);
await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => {
ingresses = res.body.items;
});
} catch {
logger.log("Can't connect to the Kubernetes API", 'ERROR');
}
if (ingresses) {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
ingresses = ingresses.filter(
(e) => Object.keys(e.metadata.annotations).length !== 0
);
const kubernetesApps = [];
for (const ingress of ingresses) {
const annotations = ingress.metadata.annotations;
if (
'flame.pawelmalak/name' in annotations &&
'flame.pawelmalak/url' in annotations &&
/^app/.test(annotations['flame.pawelmalak/type'])
) {
kubernetesApps.push({
name: annotations['flame.pawelmalak/name'],
url: annotations['flame.pawelmalak/url'],
icon: annotations['flame.pawelmalak/icon'] || 'kubernetes',
});
}
}
if (unpinStoppedApps) {
for (const app of apps) {
await app.update({ isPinned: false });
}
}
for (const item of kubernetesApps) {
if (apps.some((app) => app.name === item.name)) {
const app = apps.find((a) => a.name === item.name);
await app.update({ ...item, isPinned: true });
} else {
await App.create({
...item,
isPinned: true,
});
}
}
}
};
module.exports = useKubernetes;

View file

@ -0,0 +1,52 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
const { Sequelize } = require('sequelize');
const loadConfig = require('../../utils/loadConfig');
const { useKubernetes, useDocker } = require('./docker');
// @desc Get all apps
// @route GET /api/apps
// @access Public
const getAllApps = asyncWrapper(async (req, res, next) => {
const {
useOrdering: orderType,
dockerApps: useDockerAPI,
kubernetesApps: useKubernetesAPI,
} = await loadConfig();
let apps;
if (useDockerAPI) {
await useDocker(apps);
}
if (useKubernetesAPI) {
await useKubernetes(apps);
}
if (orderType == 'name') {
apps = await App.findAll({
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
});
} else {
apps = await App.findAll({
order: [[orderType, 'ASC']],
});
}
if (process.env.NODE_ENV === 'production') {
// Set header to fetch containers info every time
return res.status(200).setHeader('Cache-Control', 'no-store').json({
success: true,
data: apps,
});
}
res.status(200).json({
success: true,
data: apps,
});
});
module.exports = getAllApps;

View file

@ -0,0 +1,27 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
// @desc Get single app
// @route GET /api/apps/:id
// @access Public
const getSingleApp = asyncWrapper(async (req, res, next) => {
const app = await App.findOne({
where: { id: req.params.id },
});
if (!app) {
return next(
new ErrorResponse(
`App with the id of ${req.params.id} was not found`,
404
)
);
}
res.status(200).json({
success: true,
data: app,
});
});
module.exports = getSingleApp;

View file

@ -0,0 +1,8 @@
module.exports = {
createApp: require('./createApp'),
getSingleApp: require('./getSingleApp'),
deleteApp: require('./deleteApp'),
updateApp: require('./updateApp'),
reorderApps: require('./reorderApps'),
getAllApps: require('./getAllApps'),
};

View file

@ -0,0 +1,23 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
// @desc Reorder apps
// @route PUT /api/apps/0/reorder
// @access Public
const reorderApps = asyncWrapper(async (req, res, next) => {
req.body.apps.forEach(async ({ id, orderId }) => {
await App.update(
{ orderId },
{
where: { id },
}
);
});
res.status(200).json({
success: true,
data: {},
});
});
module.exports = reorderApps;

View file

@ -0,0 +1,35 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const App = require('../../models/App');
// @desc Update app
// @route PUT /api/apps/:id
// @access Public
const updateApp = asyncWrapper(async (req, res, next) => {
let app = await App.findOne({
where: { id: req.params.id },
});
if (!app) {
return next(
new ErrorResponse(
`App with the id of ${req.params.id} was not found`,
404
)
);
}
let _body = { ...req.body };
if (req.file) {
_body.icon = req.file.filename;
}
app = await app.update(_body);
res.status(200).json({
success: true,
data: app,
});
});
module.exports = updateApp;

View file

@ -1,112 +0,0 @@
const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const Bookmark = require('../models/Bookmark');
const { Sequelize } = require('sequelize');
// @desc Create new bookmark
// @route POST /api/bookmarks
// @access Public
exports.createBookmark = asyncWrapper(async (req, res, next) => {
let bookmark;
let _body = {
...req.body,
categoryId: parseInt(req.body.categoryId),
};
if (req.file) {
_body.icon = req.file.filename;
}
bookmark = await Bookmark.create(_body);
res.status(201).json({
success: true,
data: bookmark,
});
});
// @desc Get all bookmarks
// @route GET /api/bookmarks
// @access Public
exports.getBookmarks = asyncWrapper(async (req, res, next) => {
const bookmarks = await Bookmark.findAll({
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
});
res.status(200).json({
success: true,
data: bookmarks,
});
});
// @desc Get single bookmark
// @route GET /api/bookmarks/:id
// @access Public
exports.getBookmark = asyncWrapper(async (req, res, next) => {
const bookmark = await Bookmark.findOne({
where: { id: req.params.id },
});
if (!bookmark) {
return next(
new ErrorResponse(
`Bookmark with id of ${req.params.id} was not found`,
404
)
);
}
res.status(200).json({
success: true,
data: bookmark,
});
});
// @desc Update bookmark
// @route PUT /api/bookmarks/:id
// @access Public
exports.updateBookmark = asyncWrapper(async (req, res, next) => {
let bookmark = await Bookmark.findOne({
where: { id: req.params.id },
});
if (!bookmark) {
return next(
new ErrorResponse(
`Bookmark with id of ${req.params.id} was not found`,
404
)
);
}
let _body = {
...req.body,
categoryId: parseInt(req.body.categoryId),
};
if (req.file) {
_body.icon = req.file.filename;
}
bookmark = await bookmark.update(_body);
res.status(200).json({
success: true,
data: bookmark,
});
});
// @desc Delete bookmark
// @route DELETE /api/bookmarks/:id
// @access Public
exports.deleteBookmark = asyncWrapper(async (req, res, next) => {
await Bookmark.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});

View file

@ -0,0 +1,27 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Bookmark = require('../../models/Bookmark');
// @desc Create new bookmark
// @route POST /api/bookmarks
// @access Public
const createBookmark = asyncWrapper(async (req, res, next) => {
let bookmark;
let _body = {
...req.body,
categoryId: parseInt(req.body.categoryId),
};
if (req.file) {
_body.icon = req.file.filename;
}
bookmark = await Bookmark.create(_body);
res.status(201).json({
success: true,
data: bookmark,
});
});
module.exports = createBookmark;

View file

@ -0,0 +1,18 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Bookmark = require('../../models/Bookmark');
// @desc Delete bookmark
// @route DELETE /api/bookmarks/:id
// @access Public
const deleteBookmark = asyncWrapper(async (req, res, next) => {
await Bookmark.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});
module.exports = deleteBookmark;

View file

@ -0,0 +1,19 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Bookmark = require('../../models/Bookmark');
const { Sequelize } = require('sequelize');
// @desc Get all bookmarks
// @route GET /api/bookmarks
// @access Public
const getAllBookmarks = asyncWrapper(async (req, res, next) => {
const bookmarks = await Bookmark.findAll({
order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']],
});
res.status(200).json({
success: true,
data: bookmarks,
});
});
module.exports = getAllBookmarks;

View file

@ -0,0 +1,28 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const Bookmark = require('../../models/Bookmark');
// @desc Get single bookmark
// @route GET /api/bookmarks/:id
// @access Public
const getSingleBookmark = asyncWrapper(async (req, res, next) => {
const bookmark = await Bookmark.findOne({
where: { id: req.params.id },
});
if (!bookmark) {
return next(
new ErrorResponse(
`Bookmark with the id of ${req.params.id} was not found`,
404
)
);
}
res.status(200).json({
success: true,
data: bookmark,
});
});
module.exports = getSingleBookmark;

View file

@ -0,0 +1,7 @@
module.exports = {
createBookmark: require('./createBookmark'),
getAllBookmarks: require('./getAllBookmarks'),
getSingleBookmark: require('./getSingleBookmark'),
updateBookmark: require('./updateBookmark'),
deleteBookmark: require('./deleteBookmark'),
};

View file

@ -0,0 +1,39 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const Bookmark = require('../../models/Bookmark');
// @desc Update bookmark
// @route PUT /api/bookmarks/:id
// @access Public
const updateBookmark = asyncWrapper(async (req, res, next) => {
let bookmark = await Bookmark.findOne({
where: { id: req.params.id },
});
if (!bookmark) {
return next(
new ErrorResponse(
`Bookmark with id of ${req.params.id} was not found`,
404
)
);
}
let _body = {
...req.body,
categoryId: parseInt(req.body.categoryId),
};
if (req.file) {
_body.icon = req.file.filename;
}
bookmark = await bookmark.update(_body);
res.status(200).json({
success: true,
data: bookmark,
});
});
module.exports = updateBookmark;

View file

@ -4,20 +4,17 @@ const Category = require('../models/Category');
const Bookmark = require('../models/Bookmark'); const Bookmark = require('../models/Bookmark');
const Config = require('../models/Config'); const Config = require('../models/Config');
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const loadConfig = require('../utils/loadConfig');
// @desc Create new category // @desc Create new category
// @route POST /api/categories // @route POST /api/categories
// @access Public // @access Public
exports.createCategory = asyncWrapper(async (req, res, next) => { exports.createCategory = asyncWrapper(async (req, res, next) => {
// Get config from database const { pinCategoriesByDefault: pinCategories } = await loadConfig();
const pinCategories = await Config.findOne({
where: { key: 'pinCategoriesByDefault' },
});
let category; let category;
if (pinCategories) { if (pinCategories) {
if (parseInt(pinCategories.value)) {
category = await Category.create({ category = await Category.create({
...req.body, ...req.body,
isPinned: true, isPinned: true,
@ -25,7 +22,6 @@ exports.createCategory = asyncWrapper(async (req, res, next) => {
} else { } else {
category = await Category.create(req.body); category = await Category.create(req.body);
} }
}
res.status(201).json({ res.status(201).json({
success: true, success: true,
@ -37,12 +33,8 @@ exports.createCategory = asyncWrapper(async (req, res, next) => {
// @route GET /api/categories // @route GET /api/categories
// @access Public // @access Public
exports.getCategories = asyncWrapper(async (req, res, next) => { exports.getCategories = asyncWrapper(async (req, res, next) => {
// Get config from database const { useOrdering: orderType } = await loadConfig();
const useOrdering = await Config.findOne({
where: { key: 'useOrdering' },
});
const orderType = useOrdering ? useOrdering.value : 'createdAt';
let categories; let categories;
if (orderType == 'name') { if (orderType == 'name') {

View file

@ -1,177 +0,0 @@
const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const Config = require('../models/Config');
const { Op } = require('sequelize');
const File = require('../utils/File');
const { join } = require('path');
const fs = require('fs');
// @desc Insert new key:value pair
// @route POST /api/config
// @access Public
exports.createPair = asyncWrapper(async (req, res, next) => {
const pair = await Config.create(req.body);
res.status(201).json({
success: true,
data: pair,
});
});
// @desc Get all key:value pairs
// @route GET /api/config
// @route GET /api/config?keys=foo,bar,baz
// @access Public
exports.getAllPairs = asyncWrapper(async (req, res, next) => {
let pairs;
if (req.query.keys) {
// Check for specific keys to get in a single query
const keys = req.query.keys.split(',').map((key) => {
return { key };
});
pairs = await Config.findAll({
where: {
[Op.or]: keys,
},
});
} else {
// Else get all
pairs = await Config.findAll();
}
res.status(200).json({
success: true,
data: pairs,
});
});
// @desc Get single key:value pair
// @route GET /api/config/:key
// @access Public
exports.getSinglePair = asyncWrapper(async (req, res, next) => {
const pair = await Config.findOne({
where: { key: req.params.key },
});
if (!pair) {
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
}
res.status(200).json({
success: true,
data: pair,
});
});
// @desc Update value
// @route PUT /api/config/:key
// @access Public
exports.updateValue = asyncWrapper(async (req, res, next) => {
let pair = await Config.findOne({
where: { key: req.params.key },
});
if (!pair) {
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
}
if (pair.isLocked) {
return next(
new ErrorResponse(
`Value of key ${req.params.key} is locked and can not be changed`,
400
)
);
}
pair = await pair.update({ ...req.body });
res.status(200).json({
success: true,
data: pair,
});
});
// @desc Update multiple values
// @route PUT /api/config/
// @access Public
exports.updateValues = asyncWrapper(async (req, res, next) => {
Object.entries(req.body).forEach(async ([key, value]) => {
await Config.update(
{ value },
{
where: { key },
}
);
});
const config = await Config.findAll();
res.status(200).send({
success: true,
data: config,
});
});
// @desc Delete key:value pair
// @route DELETE /api/config/:key
// @access Public
exports.deletePair = asyncWrapper(async (req, res, next) => {
const pair = await Config.findOne({
where: { key: req.params.key },
});
if (!pair) {
return next(new ErrorResponse(`Key ${req.params.key} was not found`, 404));
}
if (pair.isLocked) {
return next(
new ErrorResponse(
`Value of key ${req.params.key} is locked and can not be deleted`,
400
)
);
}
await pair.destroy();
res.status(200).json({
success: true,
data: {},
});
});
// @desc Get custom CSS file
// @route GET /api/config/0/css
// @access Public
exports.getCss = asyncWrapper(async (req, res, next) => {
const file = new File(join(__dirname, '../public/flame.css'));
const content = file.read();
res.status(200).json({
success: true,
data: content,
});
});
// @desc Update custom CSS file
// @route PUT /api/config/0/css
// @access Public
exports.updateCss = asyncWrapper(async (req, res, next) => {
const file = new File(join(__dirname, '../public/flame.css'));
file.write(req.body.styles, false);
// Copy file to docker volume
fs.copyFileSync(
join(__dirname, '../public/flame.css'),
join(__dirname, '../data/flame.css')
);
res.status(200).json({
success: true,
data: {},
});
});

View file

@ -0,0 +1,18 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
const { join } = require('path');
// @desc Get custom CSS file
// @route GET /api/config/0/css
// @access Public
const getCSS = asyncWrapper(async (req, res, next) => {
const file = new File(join(__dirname, '../../public/flame.css'));
const content = file.read();
res.status(200).json({
success: true,
data: content,
});
});
module.exports = getCSS;

View file

@ -0,0 +1,16 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const loadConfig = require('../../utils/loadConfig');
// @desc Get config
// @route GET /api/config
// @access Public
const getConfig = asyncWrapper(async (req, res, next) => {
const config = await loadConfig();
res.status(200).json({
success: true,
data: config,
});
});
module.exports = getConfig;

View file

@ -0,0 +1,6 @@
module.exports = {
getCSS: require('./getCSS'),
updateCSS: require('./updateCSS'),
getConfig: require('./getConfig'),
updateConfig: require('./updateConfig'),
};

View file

@ -0,0 +1,24 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
const { join } = require('path');
// @desc Update custom CSS file
// @route PUT /api/config/0/css
// @access Public
const updateCSS = asyncWrapper(async (req, res, next) => {
const file = new File(join(__dirname, '../../public/flame.css'));
file.write(req.body.styles, false);
// Copy file to docker volume
fs.copyFileSync(
join(__dirname, '../../public/flame.css'),
join(__dirname, '../../data/flame.css')
);
res.status(200).json({
success: true,
data: {},
});
});
module.exports = updateCSS;

View file

@ -0,0 +1,24 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const loadConfig = require('../../utils/loadConfig');
const { writeFile } = require('fs/promises');
// @desc Update config
// @route PUT /api/config/
// @access Public
const updateConfig = asyncWrapper(async (req, res, next) => {
const existingConfig = await loadConfig();
const newConfig = {
...existingConfig,
...req.body,
};
await writeFile('data/config.json', JSON.stringify(newConfig));
res.status(200).send({
success: true,
data: newConfig,
});
});
module.exports = updateConfig;

View file

@ -1,6 +1,5 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const { join } = require('path'); const { join } = require('path');
const fs = require('fs');
const Umzug = require('umzug'); const Umzug = require('umzug');
const backupDB = require('./utils/backupDb'); const backupDB = require('./utils/backupDb');

View file

@ -0,0 +1,37 @@
const { readFile, writeFile, copyFile } = require('fs/promises');
const Config = require('../../models/Config');
const up = async (query) => {
await copyFile('utils/init/initialConfig.json', 'data/config.json');
const initConfigFile = await readFile('data/config.json', 'utf-8');
const parsedNewConfig = JSON.parse(initConfigFile);
const existingConfig = await Config.findAll({ raw: true });
for (let pair of existingConfig) {
const { key, value, valueType } = pair;
let newValue = value;
if (valueType == 'number') {
newValue = parseFloat(value);
} else if (valueType == 'boolean') {
newValue = value == 1;
}
parsedNewConfig[key] = newValue;
}
const newConfig = JSON.stringify(parsedNewConfig);
await writeFile('data/config.json', newConfig);
await query.dropTable('config');
};
const down = async (query) => {};
module.exports = {
up,
down,
};

View file

@ -1,17 +1,7 @@
// const asyncWrapper = foo => (req, res, next) => {
// return Promise
// .resolve(foo(req, res, next))
// .catch(next);
// }
// module.exports = asyncWrapper;
function asyncWrapper(foo) { function asyncWrapper(foo) {
return function (req, res, next) { return function (req, res, next) {
return Promise return Promise.resolve(foo(req, res, next)).catch(next);
.resolve(foo(req, res, next)) };
.catch(next);
}
} }
module.exports = asyncWrapper; module.exports = asyncWrapper;

View file

@ -14,10 +14,14 @@ const errorHandler = (err, req, res, next) => {
logger.log(error.message.split(',')[0], 'ERROR'); logger.log(error.message.split(',')[0], 'ERROR');
res.status(err.statusCode || 500).json({ if (process.env.NODE_ENV == 'development') {
success: false, console.log(err);
error: error.message || 'Server Error'
})
} }
res.status(err.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
});
};
module.exports = errorHandler; module.exports = errorHandler;

View file

@ -11,7 +11,7 @@ const storage = multer.diskStorage({
}, },
filename: (req, file, cb) => { filename: (req, file, cb) => {
cb(null, Date.now() + '--' + file.originalname); cb(null, Date.now() + '--' + file.originalname);
} },
}); });
const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml']; const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml'];

View file

@ -4,26 +4,17 @@ const upload = require('../middleware/multer');
const { const {
createApp, createApp,
getApps, getAllApps,
getApp, getSingleApp,
updateApp, updateApp,
deleteApp, deleteApp,
reorderApps reorderApps,
} = require('../controllers/apps'); } = require('../controllers/apps');
router router.route('/').post(upload, createApp).get(getAllApps);
.route('/')
.post(upload, createApp)
.get(getApps);
router router.route('/:id').get(getSingleApp).put(upload, updateApp).delete(deleteApp);
.route('/:id')
.get(getApp)
.put(upload, updateApp)
.delete(deleteApp);
router router.route('/0/reorder').put(reorderApps);
.route('/0/reorder')
.put(reorderApps);
module.exports = router; module.exports = router;

View file

@ -4,20 +4,17 @@ const upload = require('../middleware/multer');
const { const {
createBookmark, createBookmark,
getBookmarks, getAllBookmarks,
getBookmark, getSingleBookmark,
updateBookmark, updateBookmark,
deleteBookmark deleteBookmark,
} = require('../controllers/bookmark'); } = require('../controllers/bookmarks');
router router.route('/').post(upload, createBookmark).get(getAllBookmarks);
.route('/')
.post(upload, createBookmark)
.get(getBookmarks);
router router
.route('/:id') .route('/:id')
.get(getBookmark) .get(getSingleBookmark)
.put(upload, updateBookmark) .put(upload, updateBookmark)
.delete(deleteBookmark); .delete(deleteBookmark);

View file

@ -2,20 +2,14 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const { const {
createPair, getCSS,
getAllPairs, updateCSS,
getSinglePair, getConfig,
updateValue, updateConfig,
updateValues,
deletePair,
updateCss,
getCss,
} = require('../controllers/config'); } = require('../controllers/config');
router.route('/').post(createPair).get(getAllPairs).put(updateValues); router.route('/').get(getConfig).put(updateConfig);
router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair); router.route('/0/css').get(getCSS).put(updateCSS);
router.route('/0/css').get(getCss).put(updateCss);
module.exports = router; module.exports = router;

10
utils/checkFileExists.js Normal file
View file

@ -0,0 +1,10 @@
const fs = require('fs');
const checkFileExists = (path) => {
return fs.promises
.access(path, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
};
module.exports = checkFileExists;

View file

@ -1,15 +1,9 @@
const Config = require('../models/Config');
const Weather = require('../models/Weather'); const Weather = require('../models/Weather');
const axios = require('axios'); const axios = require('axios');
const loadConfig = require('./loadConfig');
const getExternalWeather = async () => { const getExternalWeather = async () => {
// Get config from database const { WEATHER_API_KEY: secret, lat, long } = await loadConfig();
const config = await Config.findAll();
// Find and check values
const secret = config.find(pair => pair.key === 'WEATHER_API_KEY');
const lat = config.find(pair => pair.key === 'lat');
const long = config.find(pair => pair.key === 'long');
if (!secret) { if (!secret) {
throw new Error('API key was not found. Weather updated failed'); throw new Error('API key was not found. Weather updated failed');
@ -21,7 +15,9 @@ const getExternalWeather = async () => {
// Fetch data from external API // Fetch data from external API
try { try {
const res = await axios.get(`http://api.weatherapi.com/v1/current.json?key=${secret.value}&q=${lat.value},${long.value}`); const res = await axios.get(
`http://api.weatherapi.com/v1/current.json?key=${secret}&q=${lat},${long}`
);
// Save weather data // Save weather data
const cursor = res.data.current; const cursor = res.data.current;
@ -32,12 +28,12 @@ const getExternalWeather = async () => {
isDay: cursor.is_day, isDay: cursor.is_day,
cloud: cursor.cloud, cloud: cursor.cloud,
conditionText: cursor.condition.text, conditionText: cursor.condition.text,
conditionCode: cursor.condition.code conditionCode: cursor.condition.code,
}); });
return weatherData; return weatherData;
} catch (err) { } catch (err) {
throw new Error('External API request failed'); throw new Error('External API request failed');
} }
} };
module.exports = getExternalWeather; module.exports = getExternalWeather;

View file

@ -2,8 +2,8 @@ const initConfig = require('./initConfig');
const initFiles = require('./initFiles'); const initFiles = require('./initFiles');
const initApp = async () => { const initApp = async () => {
await initConfig();
await initFiles(); await initFiles();
await initConfig();
}; };
module.exports = initApp; module.exports = initApp;

View file

@ -1,39 +1,25 @@
const { Op } = require('sequelize'); const { copyFile, readFile, writeFile } = require('fs/promises');
const Config = require('../../models/Config'); const checkFileExists = require('../checkFileExists');
const { config } = require('./initialConfig.json'); const initialConfig = require('./initialConfig.json');
const Logger = require('../Logger');
const logger = new Logger();
const initConfig = async () => { const initConfig = async () => {
// Get config values const configExists = await checkFileExists('data/config.json');
const configPairs = await Config.findAll({
where: {
key: {
[Op.or]: config.map((pair) => pair.key),
},
},
});
// Get key from each pair if (!configExists) {
const configKeys = configPairs.map((pair) => pair.key); await copyFile('utils/init/initialConfig.json', 'data/config.json');
// Create missing pairs
config.forEach(async ({ key, value }) => {
if (!configKeys.includes(key)) {
await Config.create({
key,
value,
valueType: typeof value,
});
}
});
if (process.env.NODE_ENV == 'development') {
logger.log('Initial config created');
} }
return; const existingConfig = await readFile('data/config.json', 'utf-8');
const parsedConfig = JSON.parse(existingConfig);
// Add new config pairs if necessary
for (let key in initialConfig) {
if (!Object.keys(parsedConfig).includes(key)) {
parsedConfig[key] = initialConfig[key];
}
}
await writeFile('data/config.json', JSON.stringify(parsedConfig));
}; };
module.exports = initConfig; module.exports = initConfig;

View file

@ -1,84 +1,22 @@
{ {
"config": [ "WEATHER_API_KEY": "",
{ "lat": 0,
"key": "WEATHER_API_KEY", "long": 0,
"value": "" "isCelsius": true,
}, "customTitle": "Flame",
{ "pinAppsByDefault": true,
"key": "lat", "pinCategoriesByDefault": true,
"value": 0 "hideHeader": false,
}, "useOrdering": "createdAt",
{ "appsSameTab": false,
"key": "long", "bookmarksSameTab": false,
"value": 0 "searchSameTab": false,
}, "hideApps": false,
{ "hideCategories": false,
"key": "isCelsius", "hideSearch": false,
"value": true "defaultSearchProvider": "l",
}, "dockerApps": false,
{ "dockerHost": "localhost",
"key": "customTitle", "kubernetesApps": false,
"value": "Flame" "unpinStoppedApps": false
},
{
"key": "pinAppsByDefault",
"value": true
},
{
"key": "pinCategoriesByDefault",
"value": true
},
{
"key": "hideHeader",
"value": false
},
{
"key": "useOrdering",
"value": "createdAt"
},
{
"key": "appsSameTab",
"value": false
},
{
"key": "bookmarksSameTab",
"value": false
},
{
"key": "searchSameTab",
"value": false
},
{
"key": "hideApps",
"value": false
},
{
"key": "hideCategories",
"value": false
},
{
"key": "hideSearch",
"value": false
},
{
"key": "defaultSearchProvider",
"value": "l"
},
{
"key": "dockerApps",
"value": false
},
{
"key": "dockerHost",
"value": "localhost"
},
{
"key": "kubernetesApps",
"value": false
},
{
"key": "unpinStoppedApps",
"value": false
}
]
} }

18
utils/loadConfig.js Normal file
View file

@ -0,0 +1,18 @@
const { readFile } = require('fs/promises');
const checkFileExists = require('../utils/checkFileExists');
const initConfig = require('../utils/init/initConfig');
const loadConfig = async () => {
const configExists = await checkFileExists('data/config.json');
if (!configExists) {
await initConfig();
}
const config = await readFile('data/config.json', 'utf-8');
const parsedConfig = JSON.parse(config);
return parsedConfig;
};
module.exports = loadConfig;