From 16341ca6da3bce3764ce871e0de6c89ae12210aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Adam=20Sowa?= Date: Sun, 10 Oct 2021 13:06:48 +0200 Subject: [PATCH 1/8] Support traefik labels for URL configuration --- controllers/apps.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/controllers/apps.js b/controllers/apps.js index 4376d15..cb458a3 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -96,7 +96,33 @@ exports.getApps = asyncWrapper(async (req, res, next) => { containers = containers.filter((e) => Object.keys(e.Labels).length !== 0); const dockerApps = []; for (const container of containers) { - const labels = container.Labels; + let labels = container.Labels; + + 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(';'); + } + } + } + } + } if ( 'flame.name' in labels && From e5cba605fa2623b20124e86da24201de89d0557d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 13 Oct 2021 13:31:01 +0200 Subject: [PATCH 2/8] Search bar bug fixes --- CHANGELOG.md | 4 +++ client/src/components/SearchBar/SearchBar.tsx | 27 ++++++++++++++----- client/src/store/reducers/theme.ts | 24 +++++++++-------- client/src/utility/searchParser.ts | 2 +- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c68e1..54d5274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### v1.7.1 (TBA) +- Fixed search action not being triggered by Numpad Enter +- Fixed search bar not redirecting to valid URL if it starts with capital letter ([#118](https://github.com/pawelmalak/flame/issues/118)) + ### 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)) - Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71)) diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 887a2ef..85175ff 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -27,6 +27,11 @@ const SearchBar = (props: ComponentProps): JSX.Element => { inputRef.current.focus(); }, []); + const clearSearch = () => { + inputRef.current.value = ''; + setLocalSearch(''); + }; + const searchHandler = (e: KeyboardEvent) => { const { isLocal, search, query, isURL, sameTab } = searchParser( inputRef.current.value @@ -36,31 +41,39 @@ const SearchBar = (props: ComponentProps): JSX.Element => { setLocalSearch(search); } - if (e.code === 'Enter') { + if (e.code === 'Enter' || e.code === 'NumpadEnter') { if (!query.prefix) { + // Prefix not found -> emit notification createNotification({ title: 'Error', message: 'Prefix not found', }); } else if (isURL) { + // URL or IP passed -> redirect const url = urlParser(inputRef.current.value)[1]; redirectUrl(url, sameTab); } else if (isLocal) { + // Local query -> filter apps and bookmarks setLocalSearch(search); } else { + // Valid query -> redirect to search results const url = `${query.template}${search}`; redirectUrl(url, sameTab); } + } else if (e.code === 'Escape') { + clearSearch(); } }; return ( - searchHandler(e)} - /> +
+ searchHandler(e)} + /> +
); }; diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index fabcc4b..6adc225 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -7,20 +7,22 @@ export interface State { const initialState: State = { theme: { - name: 'blues', + name: 'tron', colors: { - background: '#2B2C56', - primary: '#EFF1FC', - accent: '#6677EB' - } - } -} + background: '#242B33', + primary: '#EFFBFF', + accent: '#6EE2FF', + }, + }, +}; const themeReducer = (state = initialState, action: Action) => { switch (action.type) { - case ActionTypes.setTheme: return { theme: action.payload }; - default: return state; + case ActionTypes.setTheme: + return { theme: action.payload }; + default: + return state; } -} +}; -export default themeReducer; \ No newline at end of file +export default themeReducer; diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index 2befdd2..e14617c 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -20,7 +20,7 @@ export const searchParser = (searchQuery: string): SearchResult => { // Check if url or ip was passed 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); From b7de1e3d275dba1ffc7f66d482579153989f835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 21 Oct 2021 17:14:25 +0200 Subject: [PATCH 3/8] Server: Reimplemented config system --- db/migrations/01_new-config.js | 41 +++++++++++++ utils/checkFileExists.js | 10 ++++ utils/init/index.js | 2 +- utils/init/initConfig.js | 48 ++++++---------- utils/init/initialConfig.json | 102 +++++++-------------------------- utils/loadConfig.js | 18 ++++++ 6 files changed, 107 insertions(+), 114 deletions(-) create mode 100644 db/migrations/01_new-config.js create mode 100644 utils/checkFileExists.js create mode 100644 utils/loadConfig.js diff --git a/db/migrations/01_new-config.js b/db/migrations/01_new-config.js new file mode 100644 index 0000000..2c42af7 --- /dev/null +++ b/db/migrations/01_new-config.js @@ -0,0 +1,41 @@ +const { DataTypes } = require('sequelize'); +const { INTEGER, DATE, STRING, TINYINT, FLOAT, TEXT } = DataTypes; +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) => { + // await query.dropTable('config'); +}; + +module.exports = { + up, + down, +}; diff --git a/utils/checkFileExists.js b/utils/checkFileExists.js new file mode 100644 index 0000000..091c24e --- /dev/null +++ b/utils/checkFileExists.js @@ -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; diff --git a/utils/init/index.js b/utils/init/index.js index a0e11a1..bbc507c 100644 --- a/utils/init/index.js +++ b/utils/init/index.js @@ -2,8 +2,8 @@ const initConfig = require('./initConfig'); const initFiles = require('./initFiles'); const initApp = async () => { - await initConfig(); await initFiles(); + await initConfig(); }; module.exports = initApp; diff --git a/utils/init/initConfig.js b/utils/init/initConfig.js index 83ce4ea..b7ef5d9 100644 --- a/utils/init/initConfig.js +++ b/utils/init/initConfig.js @@ -1,39 +1,25 @@ -const { Op } = require('sequelize'); -const Config = require('../../models/Config'); -const { config } = require('./initialConfig.json'); - -const Logger = require('../Logger'); -const logger = new Logger(); +const { copyFile, readFile, writeFile } = require('fs/promises'); +const checkFileExists = require('../checkFileExists'); +const initialConfig = require('./initialConfig.json'); const initConfig = async () => { - // Get config values - const configPairs = await Config.findAll({ - where: { - key: { - [Op.or]: config.map((pair) => pair.key), - }, - }, - }); + const configExists = await checkFileExists('data/config.json'); - // Get key from each pair - const configKeys = configPairs.map((pair) => pair.key); - - // 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'); + if (!configExists) { + await copyFile('utils/init/initialConfig.json', 'data/config.json'); } - 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; diff --git a/utils/init/initialConfig.json b/utils/init/initialConfig.json index 18cc3b4..f6b57a3 100644 --- a/utils/init/initialConfig.json +++ b/utils/init/initialConfig.json @@ -1,84 +1,22 @@ { - "config": [ - { - "key": "WEATHER_API_KEY", - "value": "" - }, - { - "key": "lat", - "value": 0 - }, - { - "key": "long", - "value": 0 - }, - { - "key": "isCelsius", - "value": true - }, - { - "key": "customTitle", - "value": "Flame" - }, - { - "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 - } - ] + "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 } diff --git a/utils/loadConfig.js b/utils/loadConfig.js new file mode 100644 index 0000000..dc234f1 --- /dev/null +++ b/utils/loadConfig.js @@ -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; From 34279c8b8c19cd9536da1d7f55b15382de308b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 22 Oct 2021 00:42:27 +0200 Subject: [PATCH 4/8] Split apps controllers into separate files --- controllers/apps.js | 352 ----------------------- controllers/apps/createApp.js | 33 +++ controllers/apps/deleteApp.js | 18 ++ controllers/apps/docker/index.js | 4 + controllers/apps/docker/useDocker.js | 148 ++++++++++ controllers/apps/docker/useKubernetes.js | 70 +++++ controllers/apps/getAllApps.js | 52 ++++ controllers/apps/getSingleApp.js | 27 ++ controllers/apps/index.js | 8 + controllers/apps/reorderApps.js | 23 ++ controllers/apps/updateApp.js | 35 +++ db/index.js | 1 - middleware/asyncWrapper.js | 16 +- middleware/errorHandler.js | 12 +- routes/apps.js | 23 +- utils/getExternalWeather.js | 20 +- 16 files changed, 444 insertions(+), 398 deletions(-) delete mode 100644 controllers/apps.js create mode 100644 controllers/apps/createApp.js create mode 100644 controllers/apps/deleteApp.js create mode 100644 controllers/apps/docker/index.js create mode 100644 controllers/apps/docker/useDocker.js create mode 100644 controllers/apps/docker/useKubernetes.js create mode 100644 controllers/apps/getAllApps.js create mode 100644 controllers/apps/getSingleApp.js create mode 100644 controllers/apps/index.js create mode 100644 controllers/apps/reorderApps.js create mode 100644 controllers/apps/updateApp.js diff --git a/controllers/apps.js b/controllers/apps.js deleted file mode 100644 index 8fc7acd..0000000 --- a/controllers/apps.js +++ /dev/null @@ -1,352 +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) { - let labels = container.Labels; - - 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(';'); - } - } - } - } - } - - 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: {}, - }); -}); diff --git a/controllers/apps/createApp.js b/controllers/apps/createApp.js new file mode 100644 index 0000000..361e77e --- /dev/null +++ b/controllers/apps/createApp.js @@ -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; diff --git a/controllers/apps/deleteApp.js b/controllers/apps/deleteApp.js new file mode 100644 index 0000000..ed55729 --- /dev/null +++ b/controllers/apps/deleteApp.js @@ -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; diff --git a/controllers/apps/docker/index.js b/controllers/apps/docker/index.js new file mode 100644 index 0000000..f76a9e2 --- /dev/null +++ b/controllers/apps/docker/index.js @@ -0,0 +1,4 @@ +module.exports = { + useKubernetes: require('./useKubernetes'), + useDocker: require('./useDocker'), +}; diff --git a/controllers/apps/docker/useDocker.js b/controllers/apps/docker/useDocker.js new file mode 100644 index 0000000..fcc4379 --- /dev/null +++ b/controllers/apps/docker/useDocker.js @@ -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; + + // todo + 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; diff --git a/controllers/apps/docker/useKubernetes.js b/controllers/apps/docker/useKubernetes.js new file mode 100644 index 0000000..d9961cd --- /dev/null +++ b/controllers/apps/docker/useKubernetes.js @@ -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; diff --git a/controllers/apps/getAllApps.js b/controllers/apps/getAllApps.js new file mode 100644 index 0000000..1172e34 --- /dev/null +++ b/controllers/apps/getAllApps.js @@ -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; diff --git a/controllers/apps/getSingleApp.js b/controllers/apps/getSingleApp.js new file mode 100644 index 0000000..9a06b68 --- /dev/null +++ b/controllers/apps/getSingleApp.js @@ -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; diff --git a/controllers/apps/index.js b/controllers/apps/index.js new file mode 100644 index 0000000..01873b3 --- /dev/null +++ b/controllers/apps/index.js @@ -0,0 +1,8 @@ +module.exports = { + createApp: require('./createApp'), + getSingleApp: require('./getSingleApp'), + deleteApp: require('./deleteApp'), + updateApp: require('./updateApp'), + reorderApps: require('./reorderApps'), + getAllApps: require('./getAllApps'), +}; diff --git a/controllers/apps/reorderApps.js b/controllers/apps/reorderApps.js new file mode 100644 index 0000000..29794b3 --- /dev/null +++ b/controllers/apps/reorderApps.js @@ -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; diff --git a/controllers/apps/updateApp.js b/controllers/apps/updateApp.js new file mode 100644 index 0000000..2a996fb --- /dev/null +++ b/controllers/apps/updateApp.js @@ -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; diff --git a/db/index.js b/db/index.js index 34e715f..500a261 100644 --- a/db/index.js +++ b/db/index.js @@ -1,6 +1,5 @@ const { Sequelize } = require('sequelize'); const { join } = require('path'); -const fs = require('fs'); const Umzug = require('umzug'); const backupDB = require('./utils/backupDb'); diff --git a/middleware/asyncWrapper.js b/middleware/asyncWrapper.js index 11b3e52..9d99271 100644 --- a/middleware/asyncWrapper.js +++ b/middleware/asyncWrapper.js @@ -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) { return function (req, res, next) { - return Promise - .resolve(foo(req, res, next)) - .catch(next); - } + return Promise.resolve(foo(req, res, next)).catch(next); + }; } -module.exports = asyncWrapper; \ No newline at end of file +module.exports = asyncWrapper; diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js index 5db2bb2..de93c35 100644 --- a/middleware/errorHandler.js +++ b/middleware/errorHandler.js @@ -14,10 +14,14 @@ const errorHandler = (err, req, res, next) => { logger.log(error.message.split(',')[0], 'ERROR'); + if (process.env.NODE_ENV == 'development') { + console.log(err); + } + res.status(err.statusCode || 500).json({ success: false, - error: error.message || 'Server Error' - }) -} + error: error.message || 'Server Error', + }); +}; -module.exports = errorHandler; \ No newline at end of file +module.exports = errorHandler; diff --git a/routes/apps.js b/routes/apps.js index 37c0286..6f1e817 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -4,26 +4,17 @@ const upload = require('../middleware/multer'); const { createApp, - getApps, - getApp, + getAllApps, + getSingleApp, updateApp, deleteApp, - reorderApps + reorderApps, } = require('../controllers/apps'); -router - .route('/') - .post(upload, createApp) - .get(getApps); +router.route('/').post(upload, createApp).get(getAllApps); -router - .route('/:id') - .get(getApp) - .put(upload, updateApp) - .delete(deleteApp); +router.route('/:id').get(getSingleApp).put(upload, updateApp).delete(deleteApp); -router - .route('/0/reorder') - .put(reorderApps); +router.route('/0/reorder').put(reorderApps); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/utils/getExternalWeather.js b/utils/getExternalWeather.js index 1135ef7..8b2be8d 100644 --- a/utils/getExternalWeather.js +++ b/utils/getExternalWeather.js @@ -1,15 +1,9 @@ -const Config = require('../models/Config'); const Weather = require('../models/Weather'); const axios = require('axios'); +const loadConfig = require('./loadConfig'); const getExternalWeather = async () => { - // Get config from database - 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'); + const { WEATHER_API_KEY: secret, lat, long } = await loadConfig(); if (!secret) { throw new Error('API key was not found. Weather updated failed'); @@ -21,7 +15,9 @@ const getExternalWeather = async () => { // Fetch data from external API 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 const cursor = res.data.current; @@ -32,12 +28,12 @@ const getExternalWeather = async () => { isDay: cursor.is_day, cloud: cursor.cloud, conditionText: cursor.condition.text, - conditionCode: cursor.condition.code + conditionCode: cursor.condition.code, }); return weatherData; } catch (err) { throw new Error('External API request failed'); } -} +}; -module.exports = getExternalWeather; \ No newline at end of file +module.exports = getExternalWeather; From 76e50624e726e511d541398595f884d77e1de049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 22 Oct 2021 13:31:02 +0200 Subject: [PATCH 5/8] Client: Implemented new config system --- .../src/components/Apps/AppCard/AppCard.tsx | 19 +- .../src/components/Apps/AppTable/AppTable.tsx | 152 +++++---- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 16 +- .../Bookmarks/BookmarkTable/BookmarkTable.tsx | 296 +++++++++++------- client/src/components/Home/Home.tsx | 17 +- .../Settings/OtherSettings/OtherSettings.tsx | 99 +++--- .../CustomQueries/CustomQueries.tsx | 12 +- .../SearchSettings/SearchSettings.tsx | 39 +-- .../WeatherSettings/WeatherSettings.tsx | 154 ++++----- .../Widgets/WeatherWidget/WeatherWidget.tsx | 76 ++--- client/src/interfaces/Config.ts | 30 +- client/src/interfaces/Forms.ts | 31 +- client/src/utility/index.ts | 3 +- client/src/utility/inputHandler.ts | 39 +++ client/src/utility/searchConfig.ts | 24 -- client/src/utility/searchParser.ts | 9 +- .../utility/templateObjects/configTemplate.ts | 24 ++ client/src/utility/templateObjects/index.ts | 2 + .../templateObjects/settingsTemplate.ts | 30 ++ 19 files changed, 625 insertions(+), 447 deletions(-) create mode 100644 client/src/utility/inputHandler.ts delete mode 100644 client/src/utility/searchConfig.ts create mode 100644 client/src/utility/templateObjects/configTemplate.ts create mode 100644 client/src/utility/templateObjects/index.ts create mode 100644 client/src/utility/templateObjects/settingsTemplate.ts diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 172a680..803e5dd 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -2,12 +2,13 @@ import classes from './AppCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; import { iconParser, urlParser } from '../../../utility'; -import { App } from '../../../interfaces'; -import { searchConfig } from '../../../utility'; +import { App, Config, GlobalState } from '../../../interfaces'; +import { connect } from 'react-redux'; interface ComponentProps { app: App; pinHandler?: Function; + config: Config; } const AppCard = (props: ComponentProps): JSX.Element => { @@ -29,7 +30,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
@@ -41,8 +42,8 @@ const AppCard = (props: ComponentProps): JSX.Element => { return (
{iconEl}
@@ -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); diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index 6ef6e6c..3f68d76 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -1,13 +1,24 @@ 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'; // 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 -import { App, GlobalState, NewNotification } from '../../../interfaces'; +import { App, Config, GlobalState, NewNotification } from '../../../interfaces'; // 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 Table from '../../UI/Table/Table'; -// Utils -import { searchConfig } from '../../../utility'; - interface ComponentProps { apps: App[]; + config: Config; pinApp: (app: App) => void; deleteApp: (id: number) => void; updateAppHandler: (app: App) => void; @@ -36,38 +45,44 @@ const AppTable = (props: ComponentProps): JSX.Element => { // Copy apps array useEffect(() => { setLocalApps([...props.apps]); - }, [props.apps]) + }, [props.apps]); // Check ordering useEffect(() => { - const order = searchConfig('useOrdering', ''); + const order = props.config.useOrdering; if (order === 'orderId') { setIsCustomOrder(true); } - }, []) + }, []); 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) { props.deleteApp(app.id); } - } + }; // 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') { handler(app); } - } + }; const dragEndHanlder = (result: DropResult): void => { if (!isCustomOrder) { props.createNotification({ title: 'Error', - message: 'Custom order is disabled' - }) + message: 'Custom order is disabled', + }); return; } @@ -81,32 +96,39 @@ const AppTable = (props: ComponentProps): JSX.Element => { setLocalApps(tmpApps); props.reorderApps(tmpApps); - } + }; return (
- {isCustomOrder - ?

You can drag and drop single rows to reorder application

- :

Custom order is disabled. You can change it in settings

- } + {isCustomOrder ? ( +

You can drag and drop single rows to reorder application

+ ) : ( +

+ Custom order is disabled. You can change it in{' '} + settings +

+ )}
- + {(provided) => ( - +
{localApps.map((app: App, index): JSX.Element => { return ( - + {(provided, snapshot) => { const style = { - border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', + border: snapshot.isDragging + ? '1px solid var(--color-accent)' + : 'none', borderRadius: '4px', ...provided.draggableProps.style, }; @@ -118,63 +140,85 @@ const AppTable = (props: ComponentProps): JSX.Element => { ref={provided.innerRef} style={style} > - - - + + + {!snapshot.isDragging && ( )} - ) + ); }} - ) + ); })}
{app.name}{app.url}{app.icon}{app.name}{app.url}{app.icon}
deleteAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} - tabIndex={0}> - + onKeyDown={(e) => + keyboardActionHandler( + e, + app, + deleteAppHandler + ) + } + tabIndex={0} + > +
props.updateAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} - tabIndex={0}> - + onKeyDown={(e) => + keyboardActionHandler( + e, + app, + props.updateAppHandler + ) + } + tabIndex={0} + > +
props.pinApp(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} - tabIndex={0}> - {app.isPinned - ? - : + onKeyDown={(e) => + keyboardActionHandler(e, app, props.pinApp) } + tabIndex={0} + > + {app.isPinned ? ( + + ) : ( + + )}
)}
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { - apps: state.app.apps - } -} + apps: state.app.apps, + config: state.config.config, + }; +}; const actions = { pinApp, deleteApp, reorderApps, updateConfig, - createNotification -} + createNotification, +}; -export default connect(mapStateToProps, actions)(AppTable); \ No newline at end of file +export default connect(mapStateToProps, actions)(AppTable); diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index b332a6f..93ead02 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -1,12 +1,14 @@ -import { Bookmark, Category } from '../../../interfaces'; +import { Bookmark, Category, Config, GlobalState } from '../../../interfaces'; import classes from './BookmarkCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser, urlParser, searchConfig } from '../../../utility'; +import { iconParser, urlParser } from '../../../utility'; import { Fragment } from 'react'; +import { connect } from 'react-redux'; interface ComponentProps { category: Category; + config: Config; } const BookmarkCard = (props: ComponentProps): JSX.Element => { @@ -54,7 +56,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { return (
@@ -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); diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx index 02779d5..90c34aa 100644 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx @@ -1,13 +1,30 @@ 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'; // 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 -import { Bookmark, Category, NewNotification } from '../../../interfaces'; +import { + Bookmark, + Category, + Config, + GlobalState, + NewNotification, +} from '../../../interfaces'; import { ContentType } from '../Bookmarks'; // CSS @@ -17,12 +34,10 @@ import classes from './BookmarkTable.module.css'; import Table from '../../UI/Table/Table'; import Icon from '../../UI/Icons/Icon/Icon'; -// Utils -import { searchConfig } from '../../../utility'; - interface ComponentProps { contentType: ContentType; categories: Category[]; + config: Config; pinCategory: (category: Category) => void; deleteCategory: (id: number) => void; updateHandler: (data: Category | Bookmark) => void; @@ -38,45 +53,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { // Copy categories array useEffect(() => { setLocalCategories([...props.categories]); - }, [props.categories]) + }, [props.categories]); // Check ordering useEffect(() => { - const order = searchConfig('useOrdering', ''); + const order = props.config.useOrdering; if (order === 'orderId') { setIsCustomOrder(true); } - }) + }); 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) { props.deleteCategory(category.id); } - } + }; 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) { 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') { handler(category); } - } + }; const dragEndHanlder = (result: DropResult): void => { if (!isCustomOrder) { props.createNotification({ title: 'Error', - message: 'Custom order is disabled' - }) + message: 'Custom order is disabled', + }); return; } @@ -90,136 +113,171 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { setLocalCategories(tmpCategories); props.reorderCategories(tmpCategories); - } + }; if (props.contentType === ContentType.category) { return (
- {isCustomOrder - ?

You can drag and drop single rows to reorder categories

- :

Custom order is disabled. You can change it in settings

- } + {isCustomOrder ? ( +

You can drag and drop single rows to reorder categories

+ ) : ( +

+ Custom order is disabled. You can change it in{' '} + settings +

+ )}
- + {(provided) => ( - - {localCategories.map((category: Category, index): JSX.Element => { - return ( - - {(provided, snapshot) => { - const style = { - border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', - borderRadius: '4px', - ...provided.draggableProps.style, - }; +
+ {localCategories.map( + (category: Category, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging + ? '1px solid var(--color-accent)' + : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; - return ( - - - {!snapshot.isDragging && ( - - )} - - ) - }} - - ) - })} + return ( + + + {!snapshot.isDragging && ( + + )} + + ); + }} + + ); + } + )}
{category.name} -
deleteCategoryHandler(category)} - onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)} - tabIndex={0}> - -
-
props.updateHandler(category)} - tabIndex={0}> - -
-
props.pinCategory(category)} - onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)} - tabIndex={0}> - {category.isPinned - ? - : - } -
-
{category.name} +
+ deleteCategoryHandler(category) + } + onKeyDown={(e) => + keyboardActionHandler( + e, + category, + deleteCategoryHandler + ) + } + tabIndex={0} + > + +
+
+ props.updateHandler(category) + } + tabIndex={0} + > + +
+
props.pinCategory(category)} + onKeyDown={(e) => + keyboardActionHandler( + e, + category, + props.pinCategory + ) + } + tabIndex={0} + > + {category.isPinned ? ( + + ) : ( + + )} +
+
)}
- ) + ); } else { - const bookmarks: {bookmark: Bookmark, categoryName: string}[] = []; + const bookmarks: { bookmark: Bookmark; categoryName: string }[] = []; props.categories.forEach((category: Category) => { category.bookmarks.forEach((bookmark: Bookmark) => { bookmarks.push({ bookmark, - categoryName: category.name + categoryName: category.name, }); - }) - }) + }); + }); return ( - - {bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => { - return ( - - - - - - - - ) - })} +
{bookmark.bookmark.name}{bookmark.bookmark.url}{bookmark.bookmark.icon}{bookmark.categoryName} -
deleteBookmarkHandler(bookmark.bookmark)} - tabIndex={0}> - -
-
props.updateHandler(bookmark.bookmark)} - tabIndex={0}> - -
-
+ {bookmarks.map( + (bookmark: { bookmark: Bookmark; categoryName: string }) => { + return ( + + + + + + + + ); + } + )}
{bookmark.bookmark.name}{bookmark.bookmark.url}{bookmark.bookmark.icon}{bookmark.categoryName} +
deleteBookmarkHandler(bookmark.bookmark)} + tabIndex={0} + > + +
+
props.updateHandler(bookmark.bookmark)} + tabIndex={0} + > + +
+
- ) + ); } -} +}; + +const mapStateToProps = (state: GlobalState) => { + return { + config: state.config.config, + }; +}; const actions = { pinCategory, deleteCategory, deleteBookmark, createNotification, - reorderCategories -} + reorderCategories, +}; -export default connect(null, actions)(BookmarkTable); \ No newline at end of file +export default connect(mapStateToProps, actions)(BookmarkTable); diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index fd711aa..18d81bc 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -7,7 +7,7 @@ import { getApps, getCategories } from '../../store/actions'; // Typescript import { GlobalState } from '../../interfaces/GlobalState'; -import { App, Category } from '../../interfaces'; +import { App, Category, Config } from '../../interfaces'; // UI import Icon from '../UI/Icons/Icon/Icon'; @@ -28,9 +28,6 @@ import SearchBar from '../SearchBar/SearchBar'; import { greeter } from './functions/greeter'; import { dateTime } from './functions/dateTime'; -// Utils -import { searchConfig } from '../../utility'; - interface ComponentProps { getApps: Function; getCategories: Function; @@ -38,6 +35,7 @@ interface ComponentProps { apps: App[]; categoriesLoading: boolean; categories: Category[]; + config: Config; } const Home = (props: ComponentProps): JSX.Element => { @@ -77,7 +75,7 @@ const Home = (props: ComponentProps): JSX.Element => { let interval: any; // Start interval only when hideHeader is false - if (searchConfig('hideHeader', 0) !== 1) { + if (!props.config.hideHeader) { interval = setInterval(() => { setHeader({ dateTime: dateTime(), @@ -103,13 +101,13 @@ const Home = (props: ComponentProps): JSX.Element => { return ( - {searchConfig('hideSearch', 0) !== 1 ? ( + {!props.config.hideSearch ? ( ) : (
)} - {searchConfig('hideHeader', 0) !== 1 ? ( + {!props.config.hideHeader ? (

{header.dateTime}

@@ -124,7 +122,7 @@ const Home = (props: ComponentProps): JSX.Element => {
)} - {searchConfig('hideApps', 0) !== 1 ? ( + {!props.config.hideApps ? ( {appsLoading ? ( @@ -148,7 +146,7 @@ const Home = (props: ComponentProps): JSX.Element => {
)} - {searchConfig('hideCategories', 0) !== 1 ? ( + {!props.config.hideCategories ? ( {categoriesLoading ? ( @@ -182,6 +180,7 @@ const mapStateToProps = (state: GlobalState) => { apps: state.app.apps, categoriesLoading: state.bookmark.loading, categories: state.bookmark.categories, + config: state.config.config, }; }; diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index c3525f8..3d82fa4 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -11,9 +11,10 @@ import { // Typescript import { + Config, GlobalState, NewNotification, - SettingsForm, + OtherSettingsForm, } from '../../../interfaces'; // UI @@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button'; import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; // Utils -import { searchConfig } from '../../../utility'; +import { otherSettingsTemplate, inputHandler } from '../../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; - updateConfig: (formData: SettingsForm) => void; + updateConfig: (formData: OtherSettingsForm) => void; sortApps: () => void; sortCategories: () => void; loading: boolean; + config: Config; } const OtherSettings = (props: ComponentProps): JSX.Element => { + const { config } = props; + // Initial state - const [formData, setFormData] = useState({ - customTitle: document.title, - pinAppsByDefault: 1, - pinCategoriesByDefault: 1, - hideHeader: 0, - hideApps: 0, - hideCategories: 0, - useOrdering: 'createdAt', - appsSameTab: 0, - bookmarksSameTab: 0, - dockerApps: 1, - dockerHost: 'localhost', - kubernetesApps: 1, - unpinStoppedApps: 1, - }); + const [formData, setFormData] = useState( + otherSettingsTemplate + ); // Get config useEffect(() => { setFormData({ - customTitle: searchConfig('customTitle', 'Flame'), - 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), + ...config, }); }, [props.loading]); @@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { // Input handler const inputChangeHandler = ( e: ChangeEvent, - isNumber?: boolean + options?: { isNumber?: boolean; isBool?: boolean } ) => { - let value: string | number = e.target.value; - - if (isNumber) { - value = parseFloat(value); - } - - setFormData({ - ...formData, - [e.target.name]: value, + inputHandler({ + e, + options, + setStateHandler: setFormData, + state: formData, }); }; @@ -126,8 +102,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { inputChangeHandler(e, true)} + value={formData.pinCategoriesByDefault ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -165,8 +141,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { inputChangeHandler(e, true)} + value={formData.bookmarksSameTab ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -192,8 +168,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { inputChangeHandler(e, true)} + value={formData.hideApps ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -216,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { inputChangeHandler(e, true)} + value={formData.dockerApps ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -256,8 +232,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { inputChangeHandler(e, true)} + value={formData.kubernetesApps ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -286,6 +262,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { loading: state.config.loading, + config: state.config.config, }; }; diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx index c5dac62..a694f42 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx @@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css'; import Modal from '../../../UI/Modal/Modal'; 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 { deleteQuery, createNotification } from '../../../../store/actions'; import Button from '../../../UI/Buttons/Button/Button'; -import { searchConfig } from '../../../../utility'; interface Props { customQueries: Query[]; deleteQuery: (prefix: string) => {}; createNotification: (notification: NewNotification) => void; + config: Config; } const CustomQueries = (props: Props): JSX.Element => { @@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => { }; const deleteHandler = (query: Query) => { - const currentProvider = searchConfig('defaultSearchProvider', 'l'); + const currentProvider = props.config.defaultSearchProvider; const isCurrent = currentProvider === query.prefix; if (isCurrent) { @@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { customQueries: state.config.customQueries, + config: state.config.config, }; }; diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index b2ac422..a403fa6 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions'; // Typescript import { + Config, GlobalState, NewNotification, Query, @@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; // Utils -import { searchConfig } from '../../../utility'; +import { inputHandler, searchSettingsTemplate } from '../../../utility'; // Data import { queries } from '../../../utility/searchQueries.json'; @@ -32,22 +33,17 @@ interface Props { updateConfig: (formData: SearchForm) => void; loading: boolean; customQueries: Query[]; + config: Config; } const SearchSettings = (props: Props): JSX.Element => { // Initial state - const [formData, setFormData] = useState({ - hideSearch: 0, - defaultSearchProvider: 'l', - searchSameTab: 0, - }); + const [formData, setFormData] = useState(searchSettingsTemplate); // Get config useEffect(() => { setFormData({ - hideSearch: searchConfig('hideSearch', 0), - defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'), - searchSameTab: searchConfig('searchSameTab', 0), + ...props.config, }); }, [props.loading]); @@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => { // Input handler const inputChangeHandler = ( e: ChangeEvent, - isNumber?: boolean + options?: { isNumber?: boolean; isBool?: boolean } ) => { - let value: string | number = e.target.value; - - if (isNumber) { - value = parseFloat(value); - } - - setFormData({ - ...formData, - [e.target.name]: value, + inputHandler({ + e, + options, + setStateHandler: setFormData, + state: formData, }); }; @@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => { inputChangeHandler(e, true)} + value={formData.hideSearch ? 1 : 0} + onChange={(e) => inputChangeHandler(e, { isBool: true })} > @@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => { return { loading: state.config.loading, customQueries: state.config.customQueries, + config: state.config.config, }; }; diff --git a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx index 1378d44..04c9fa5 100644 --- a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx +++ b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx @@ -6,38 +6,40 @@ import { connect } from 'react-redux'; import { createNotification, updateConfig } from '../../../store/actions'; // Typescript -import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces'; +import { + ApiResponse, + Config, + GlobalState, + NewNotification, + Weather, + WeatherForm, +} from '../../../interfaces'; // UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; // Utils -import { searchConfig } from '../../../utility'; +import { inputHandler, weatherSettingsTemplate } from '../../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; updateConfig: (formData: WeatherForm) => void; loading: boolean; + config: Config; } const WeatherSettings = (props: ComponentProps): JSX.Element => { // Initial state - const [formData, setFormData] = useState({ - WEATHER_API_KEY: '', - lat: 0, - long: 0, - isCelsius: 1 - }) + const [formData, setFormData] = useState( + weatherSettingsTemplate + ); // Get config useEffect(() => { setFormData({ - WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''), - lat: searchConfig('lat', 0), - long: searchConfig('long', 0), - isCelsius: searchConfig('isCelsius', 1) - }) + ...props.config, + }); }, [props.loading]); // Form handler @@ -48,120 +50,124 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) { props.createNotification({ title: 'Warning', - message: 'API key is missing. Weather Module will NOT work' - }) + message: 'API key is missing. Weather Module will NOT work', + }); } // Save settings await props.updateConfig(formData); - + // Update weather - axios.get>('/api/weather/update') + axios + .get>('/api/weather/update') .then(() => { props.createNotification({ title: 'Success', - message: 'Weather updated' - }) + message: 'Weather updated', + }); }) .catch((err) => { props.createNotification({ title: 'Error', - message: err.response.data.error - }) + message: err.response.data.error, + }); }); - } + }; // Input handler - const inputChangeHandler = (e: ChangeEvent, isNumber?: boolean) => { - let value: string | number = e.target.value; - - if (isNumber) { - value = parseFloat(value); - } - - setFormData({ - ...formData, - [e.target.name]: value - }) - } + const inputChangeHandler = ( + e: ChangeEvent, + options?: { isNumber?: boolean; isBool?: boolean } + ) => { + inputHandler({ + e, + options, + setStateHandler: setFormData, + state: formData, + }); + }; return (
formSubmitHandler(e)}> - + inputChangeHandler(e)} /> Using - - {' '}Weather API + + {' '} + Weather API . Key is required for weather module to work. - + inputChangeHandler(e, true)} - step='any' - lang='en-150' + onChange={(e) => inputChangeHandler(e, { isNumber: true })} + step="any" + lang="en-150" /> You can use - {' '}latlong.net + href="https://www.latlong.net/convert-address-to-lat-long.html" + target="blank" + > + {' '} + latlong.net - + inputChangeHandler(e, true)} - step='any' - lang='en-150' + onChange={(e) => inputChangeHandler(e, { isNumber: true })} + step="any" + lang="en-150" /> - + - +
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { - loading: state.config.loading - } -} + loading: state.config.loading, + config: state.config.config, + }; +}; -export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings); \ No newline at end of file +export default connect(mapStateToProps, { createNotification, updateConfig })( + WeatherSettings +); diff --git a/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx b/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx index edf6fee..862a398 100644 --- a/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx +++ b/client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx @@ -5,7 +5,7 @@ import axios from 'axios'; import { connect } from 'react-redux'; // Typescript -import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces'; +import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces'; // CSS import classes from './WeatherWidget.module.css'; @@ -13,12 +13,9 @@ import classes from './WeatherWidget.module.css'; // UI import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon'; -// Utils -import { searchConfig } from '../../../utility'; - interface ComponentProps { configLoading: boolean; - config: Config[]; + config: Config; } const WeatherWidget = (props: ComponentProps): JSX.Element => { @@ -32,26 +29,28 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => { conditionCode: 1000, id: -1, createdAt: new Date(), - updatedAt: new Date() + updatedAt: new Date(), }); const [isLoading, setIsLoading] = useState(true); // Initial request to get data useEffect(() => { - axios.get>('/api/weather') - .then(data => { + axios + .get>('/api/weather') + .then((data) => { const weatherData = data.data.data[0]; if (weatherData) { setWeather(weatherData); } setIsLoading(false); }) - .catch(err => console.log(err)); + .catch((err) => console.log(err)); }, []); // Open socket for data updates 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 webSocketClient = new WebSocket(socketAddress); @@ -59,43 +58,44 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => { const data = JSON.parse(e.data); setWeather({ ...weather, - ...data - }) - } + ...data, + }); + }; return () => webSocketClient.close(); }, []); return (
- {(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) && - (weather.id > 0 && - ( -
- -
-
- {searchConfig('isCelsius', true) - ? {weather.tempC}°C - : {weather.tempF}°F - } - {weather.cloud}% -
-
) - ) - } + {isLoading || + props.configLoading || + (props.config.WEATHER_API_KEY && weather.id > 0 && ( + +
+ +
+
+ {props.config.isCelsius ? ( + {weather.tempC}°C + ) : ( + {weather.tempF}°F + )} + {weather.cloud}% +
+
+ ))}
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { configLoading: state.config.loading, - config: state.config.config - } -} + config: state.config.config, + }; +}; -export default connect(mapStateToProps)(WeatherWidget); \ No newline at end of file +export default connect(mapStateToProps)(WeatherWidget); diff --git a/client/src/interfaces/Config.ts b/client/src/interfaces/Config.ts index 281402c..d0152c5 100644 --- a/client/src/interfaces/Config.ts +++ b/client/src/interfaces/Config.ts @@ -1,8 +1,22 @@ -import { Model } from './'; - -export interface Config extends Model { - key: string; - value: string; - valueType: string; - isLocked: boolean; -} \ No newline at end of file +export interface Config { + WEATHER_API_KEY: string; + lat: number; + long: number; + isCelsius: boolean; + customTitle: string; + 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; +} diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 9b195da..9123d62 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -2,30 +2,27 @@ export interface WeatherForm { WEATHER_API_KEY: string; lat: number; long: number; - isCelsius: number; + isCelsius: boolean; } export interface SearchForm { - hideSearch: number; + hideSearch: boolean; defaultSearchProvider: string; - searchSameTab: number; + searchSameTab: boolean; } -export interface SettingsForm { +export interface OtherSettingsForm { customTitle: string; - pinAppsByDefault: number; - pinCategoriesByDefault: number; - hideHeader: number; - hideApps: number; - hideCategories: number; - // hideSearch: number; - // defaultSearchProvider: string; + pinAppsByDefault: boolean; + pinCategoriesByDefault: boolean; + hideHeader: boolean; + hideApps: boolean; + hideCategories: boolean; useOrdering: string; - appsSameTab: number; - bookmarksSameTab: number; - // searchSameTab: number; - dockerApps: number; + appsSameTab: boolean; + bookmarksSameTab: boolean; + dockerApps: boolean; dockerHost: string; - kubernetesApps: number; - unpinStoppedApps: number; + kubernetesApps: boolean; + unpinStoppedApps: boolean; } diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index caff9c3..ad08042 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -1,7 +1,8 @@ export * from './iconParser'; export * from './urlParser'; -export * from './searchConfig'; export * from './checkVersion'; export * from './sortData'; export * from './searchParser'; export * from './redirectUrl'; +export * from './templateObjects'; +export * from './inputHandler'; diff --git a/client/src/utility/inputHandler.ts b/client/src/utility/inputHandler.ts new file mode 100644 index 0000000..98e805a --- /dev/null +++ b/client/src/utility/inputHandler.ts @@ -0,0 +1,39 @@ +import { ChangeEvent, SetStateAction } from 'react'; + +type Event = ChangeEvent; + +interface Options { + isNumber?: boolean; + isBool?: boolean; +} + +interface Params { + e: Event; + options?: Options; + setStateHandler: (v: SetStateAction) => void; + state: T; +} + +export const inputHandler = (params: Params): 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, + }); +}; diff --git a/client/src/utility/searchConfig.ts b/client/src/utility/searchConfig.ts deleted file mode 100644 index 4e46091..0000000 --- a/client/src/utility/searchConfig.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index e14617c..cff9bfb 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -1,7 +1,6 @@ import { queries } from './searchQueries.json'; import { Query, SearchResult } from '../interfaces'; import { store } from '../store/store'; -import { searchConfig } from '.'; export const searchParser = (searchQuery: string): SearchResult => { const result: SearchResult = { @@ -16,7 +15,7 @@ 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 const urlRegex = @@ -27,9 +26,7 @@ export const searchParser = (searchQuery: string): SearchResult => { // Match prefix and query const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i); - const prefix = splitQuery - ? splitQuery[1] - : searchConfig('defaultSearchProvider', 'l'); + const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider; const search = splitQuery ? encodeURIComponent(splitQuery[2]) @@ -47,7 +44,7 @@ export const searchParser = (searchQuery: string): SearchResult => { if (prefix === 'l') { result.isLocal = true; } else { - result.sameTab = searchConfig('searchSameTab', false); + result.sameTab = config.searchSameTab; } return result; diff --git a/client/src/utility/templateObjects/configTemplate.ts b/client/src/utility/templateObjects/configTemplate.ts new file mode 100644 index 0000000..bbc7998 --- /dev/null +++ b/client/src/utility/templateObjects/configTemplate.ts @@ -0,0 +1,24 @@ +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, +}; diff --git a/client/src/utility/templateObjects/index.ts b/client/src/utility/templateObjects/index.ts new file mode 100644 index 0000000..3f2d57c --- /dev/null +++ b/client/src/utility/templateObjects/index.ts @@ -0,0 +1,2 @@ +export * from './configTemplate'; +export * from './settingsTemplate'; diff --git a/client/src/utility/templateObjects/settingsTemplate.ts b/client/src/utility/templateObjects/settingsTemplate.ts new file mode 100644 index 0000000..674931b --- /dev/null +++ b/client/src/utility/templateObjects/settingsTemplate.ts @@ -0,0 +1,30 @@ +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, +}; + +export const weatherSettingsTemplate: WeatherForm = { + WEATHER_API_KEY: '', + lat: 0, + long: 0, + isCelsius: true, +}; + +export const searchSettingsTemplate: SearchForm = { + hideSearch: false, + searchSameTab: false, + defaultSearchProvider: 'l', +}; From cfb471e578ed09bad28684dcb1a4cbf1ace4bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 22 Oct 2021 14:00:38 +0200 Subject: [PATCH 6/8] Changed config api. Split config controllers into separate files. Split bookmarks controllers into separate files --- CHANGELOG.md | 1 + client/src/store/actions/app.ts | 164 ++++---- client/src/store/actions/bookmark.ts | 428 +++++++++++---------- client/src/store/actions/config.ts | 11 +- client/src/store/reducers/config.ts | 5 +- controllers/apps/docker/useDocker.js | 8 +- controllers/bookmark.js | 112 ------ controllers/bookmarks/createBookmark.js | 27 ++ controllers/bookmarks/deleteBookmark.js | 18 + controllers/bookmarks/getAllBookmarks.js | 19 + controllers/bookmarks/getSingleBookmark.js | 28 ++ controllers/bookmarks/index.js | 7 + controllers/bookmarks/updateBookmark.js | 39 ++ controllers/category.js | 12 +- controllers/config.js | 177 --------- controllers/config/getCSS.js | 18 + controllers/config/getConfig.js | 16 + controllers/config/index.js | 6 + controllers/config/updateCSS.js | 24 ++ controllers/config/updateConfig.js | 24 ++ middleware/multer.js | 2 +- routes/bookmark.js | 17 +- routes/config.js | 18 +- 23 files changed, 579 insertions(+), 602 deletions(-) delete mode 100644 controllers/bookmark.js create mode 100644 controllers/bookmarks/createBookmark.js create mode 100644 controllers/bookmarks/deleteBookmark.js create mode 100644 controllers/bookmarks/getAllBookmarks.js create mode 100644 controllers/bookmarks/getSingleBookmark.js create mode 100644 controllers/bookmarks/index.js create mode 100644 controllers/bookmarks/updateBookmark.js delete mode 100644 controllers/config.js create mode 100644 controllers/config/getCSS.js create mode 100644 controllers/config/getConfig.js create mode 100644 controllers/config/index.js create mode 100644 controllers/config/updateCSS.js create mode 100644 controllers/config/updateConfig.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d5274..5b91cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### v1.7.1 (TBA) - Fixed search action not being triggered by Numpad Enter - 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) - Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67)) diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 3a8e7d5..b33a78b 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -5,14 +5,17 @@ import { App, ApiResponse, NewApp, Config } from '../../interfaces'; import { CreateNotificationAction } from './notification'; export interface GetAppsAction { - type: ActionTypes.getApps | ActionTypes.getAppsSuccess | ActionTypes.getAppsError; + type: + | ActionTypes.getApps + | ActionTypes.getAppsSuccess + | ActionTypes.getAppsError; payload: T; } export const getApps = () => async (dispatch: Dispatch) => { dispatch>({ type: ActionTypes.getApps, - payload: undefined + payload: undefined, }); try { @@ -20,12 +23,12 @@ export const getApps = () => async (dispatch: Dispatch) => { dispatch>({ type: ActionTypes.getAppsSuccess, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} +}; export interface PinAppAction { type: ActionTypes.pinApp; @@ -35,59 +38,64 @@ export interface PinAppAction { export const pinApp = (app: App) => async (dispatch: Dispatch) => { try { const { id, isPinned, name } = app; - const res = await axios.put>(`/api/apps/${id}`, { isPinned: !isPinned }); + const res = await axios.put>(`/api/apps/${id}`, { + isPinned: !isPinned, + }); - const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen'; + const status = isPinned + ? 'unpinned from Homescreen' + : 'pinned to Homescreen'; dispatch({ type: ActionTypes.createNotification, payload: { title: 'Success', - message: `App ${name} ${status}` - } - }) + message: `App ${name} ${status}`, + }, + }); dispatch({ type: ActionTypes.pinApp, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} +}; export interface AddAppAction { type: ActionTypes.addAppSuccess; payload: App; } -export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>('/api/apps', formData); +export const addApp = + (formData: NewApp | FormData) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>('/api/apps', formData); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `App added` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `App added`, + }, + }); - await dispatch({ - type: ActionTypes.addAppSuccess, - payload: res.data.data - }) + await dispatch({ + type: ActionTypes.addAppSuccess, + payload: res.data.data, + }); - // Sort apps - dispatch(sortApps()) - } catch (err) { - console.log(err); - } -} + // Sort apps + dispatch(sortApps()); + } catch (err) { + console.log(err); + } + }; export interface DeleteAppAction { - type: ActionTypes.deleteApp, - payload: number + type: ActionTypes.deleteApp; + payload: number; } export const deleteApp = (id: number) => async (dispatch: Dispatch) => { @@ -98,79 +106,85 @@ export const deleteApp = (id: number) => async (dispatch: Dispatch) => { type: ActionTypes.createNotification, payload: { title: 'Success', - message: 'App deleted' - } - }) + message: 'App deleted', + }, + }); dispatch({ type: ActionTypes.deleteApp, - payload: id - }) + payload: id, + }); } catch (err) { console.log(err); } -} +}; export interface UpdateAppAction { type: ActionTypes.updateApp; payload: App; } -export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.put>(`/api/apps/${id}`, formData); +export const updateApp = + (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/apps/${id}`, + formData + ); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `App updated` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `App updated`, + }, + }); - await dispatch({ - type: ActionTypes.updateApp, - payload: res.data.data - }) + await dispatch({ + type: ActionTypes.updateApp, + payload: res.data.data, + }); - // Sort apps - dispatch(sortApps()) - } catch (err) { - console.log(err); - } -} + // Sort apps + dispatch(sortApps()); + } catch (err) { + console.log(err); + } + }; export interface ReorderAppsAction { type: ActionTypes.reorderApps; - payload: App[] + payload: App[]; } interface ReorderQuery { apps: { id: number; orderId: number; - }[] + }[]; } export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => { try { - const updateQuery: ReorderQuery = { apps: [] } + const updateQuery: ReorderQuery = { apps: [] }; - apps.forEach((app, index) => updateQuery.apps.push({ - id: app.id, - orderId: index + 1 - })) + apps.forEach((app, index) => + updateQuery.apps.push({ + id: app.id, + orderId: index + 1, + }) + ); await axios.put>('/api/apps/0/reorder', updateQuery); dispatch({ type: ActionTypes.reorderApps, - payload: apps - }) + payload: apps, + }); } catch (err) { console.log(err); } -} +}; export interface SortAppsAction { type: ActionTypes.sortApps; @@ -179,13 +193,13 @@ export interface SortAppsAction { export const sortApps = () => async (dispatch: Dispatch) => { try { - const res = await axios.get>('/api/config/useOrdering'); + const res = await axios.get>('/api/config'); dispatch({ type: ActionTypes.sortApps, - payload: res.data.data.value - }) + payload: res.data.data.useOrdering, + }); } catch (err) { console.log(err); } -} \ No newline at end of file +}; diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index b4b5831..6d6fdf5 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -1,133 +1,157 @@ import axios from 'axios'; import { Dispatch } from 'redux'; 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'; /** * GET CATEGORIES */ export interface GetCategoriesAction { - type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError; + type: + | ActionTypes.getCategories + | ActionTypes.getCategoriesSuccess + | ActionTypes.getCategoriesError; payload: T; } export const getCategories = () => async (dispatch: Dispatch) => { dispatch>({ type: ActionTypes.getCategories, - payload: undefined - }) + payload: undefined, + }); try { const res = await axios.get>('/api/categories'); dispatch>({ type: ActionTypes.getCategoriesSuccess, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} +}; /** * ADD CATEGORY */ export interface AddCategoryAction { - type: ActionTypes.addCategory, - payload: Category + type: ActionTypes.addCategory; + payload: Category; } -export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>('/api/categories', formData); +export const addCategory = + (formData: NewCategory) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>( + '/api/categories', + formData + ); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${formData.name} created` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Category ${formData.name} created`, + }, + }); - dispatch({ - type: ActionTypes.addCategory, - payload: res.data.data - }) + dispatch({ + type: ActionTypes.addCategory, + payload: res.data.data, + }); - dispatch(sortCategories()); - } catch (err) { - console.log(err); - } -} + dispatch(sortCategories()); + } catch (err) { + console.log(err); + } + }; /** * ADD BOOKMARK */ export interface AddBookmarkAction { - type: ActionTypes.addBookmark, - payload: Bookmark + type: ActionTypes.addBookmark; + payload: Bookmark; } -export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { - try { - const res = await axios.post>('/api/bookmarks', formData); +export const addBookmark = + (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>( + '/api/bookmarks', + formData + ); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Bookmark created` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Bookmark created`, + }, + }); - dispatch({ - type: ActionTypes.addBookmark, - payload: res.data.data - }) - } catch (err) { - console.log(err); - } -} + dispatch({ + type: ActionTypes.addBookmark, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; /** * PIN CATEGORY */ export interface PinCategoryAction { - type: ActionTypes.pinCategory, - payload: Category + type: ActionTypes.pinCategory; + payload: Category; } -export const pinCategory = (category: Category) => async (dispatch: Dispatch) => { - try { - const { id, isPinned, name } = category; - const res = await axios.put>(`/api/categories/${id}`, { isPinned: !isPinned }); +export const pinCategory = + (category: Category) => async (dispatch: Dispatch) => { + try { + const { id, isPinned, name } = category; + const res = await axios.put>( + `/api/categories/${id}`, + { isPinned: !isPinned } + ); - const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen'; + const status = isPinned + ? 'unpinned from Homescreen' + : 'pinned to Homescreen'; - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${name} ${status}` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Category ${name} ${status}`, + }, + }); - dispatch({ - type: ActionTypes.pinCategory, - payload: res.data.data - }) - } catch (err) { - console.log(err); - } -} + dispatch({ + type: ActionTypes.pinCategory, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; /** * DELETE CATEGORY */ export interface DeleteCategoryAction { - type: ActionTypes.deleteCategory, - payload: number + type: ActionTypes.deleteCategory; + payload: number; } export const deleteCategory = (id: number) => async (dispatch: Dispatch) => { @@ -138,141 +162,151 @@ export const deleteCategory = (id: number) => async (dispatch: Dispatch) => { type: ActionTypes.createNotification, payload: { title: 'Success', - message: `Category deleted` - } - }) + message: `Category deleted`, + }, + }); dispatch({ type: ActionTypes.deleteCategory, - payload: id - }) + payload: id, + }); } catch (err) { console.log(err); } -} +}; /** * UPDATE CATEGORY */ export interface UpdateCategoryAction { - type: ActionTypes.updateCategory, - payload: Category + type: ActionTypes.updateCategory; + payload: Category; } -export const updateCategory = (id: number, formData: NewCategory) => async (dispatch: Dispatch) => { - try { - const res = await axios.put>(`/api/categories/${id}`, formData); +export const updateCategory = + (id: number, formData: NewCategory) => async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/categories/${id}`, + formData + ); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Category ${formData.name} updated` - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Category ${formData.name} updated`, + }, + }); - dispatch({ - type: ActionTypes.updateCategory, - payload: res.data.data - }) + dispatch({ + type: ActionTypes.updateCategory, + payload: res.data.data, + }); - dispatch(sortCategories()); - } catch (err) { - console.log(err); - } -} + dispatch(sortCategories()); + } catch (err) { + console.log(err); + } + }; /** * DELETE BOOKMARK */ export interface DeleteBookmarkAction { - type: ActionTypes.deleteBookmark, + type: ActionTypes.deleteBookmark; payload: { - bookmarkId: number, - categoryId: number - } + bookmarkId: number; + categoryId: number; + }; } -export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => { - try { - await axios.delete>(`/api/bookmarks/${bookmarkId}`); +export const deleteBookmark = + (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => { + try { + await axios.delete>(`/api/bookmarks/${bookmarkId}`); - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: 'Bookmark deleted' - } - }) + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: 'Bookmark deleted', + }, + }); - dispatch({ - type: ActionTypes.deleteBookmark, - payload: { - bookmarkId, - categoryId - } - }) - } catch (err) { - console.log(err); - } -} + dispatch({ + type: ActionTypes.deleteBookmark, + payload: { + bookmarkId, + categoryId, + }, + }); + } catch (err) { + console.log(err); + } + }; /** * UPDATE BOOKMARK */ export interface UpdateBookmarkAction { - type: ActionTypes.updateBookmark, - payload: Bookmark + type: ActionTypes.updateBookmark; + payload: Bookmark; } -export const updateBookmark = ( - bookmarkId: number, - formData: NewBookmark | FormData, - category: { - prev: number, - curr: number - } -) => async (dispatch: Dispatch) => { - try { - const res = await axios.put>(`/api/bookmarks/${bookmarkId}`, formData); - - dispatch({ - type: ActionTypes.createNotification, - payload: { - title: 'Success', - message: `Bookmark updated` - } - }) - - // Check if category was changed - const categoryWasChanged = category.curr !== category.prev; - - if (categoryWasChanged) { - // Delete bookmark from old category - dispatch({ - type: ActionTypes.deleteBookmark, - payload: { - bookmarkId, - categoryId: category.prev - } - }) - - // Add bookmark to the new category - dispatch({ - type: ActionTypes.addBookmark, - payload: res.data.data - }) - } else { - // Else update only name/url/icon - dispatch({ - type: ActionTypes.updateBookmark, - payload: res.data.data - }) +export const updateBookmark = + ( + bookmarkId: number, + formData: NewBookmark | FormData, + category: { + prev: number; + curr: number; } - } catch (err) { - console.log(err); - } -} + ) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/bookmarks/${bookmarkId}`, + formData + ); + + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `Bookmark updated`, + }, + }); + + // Check if category was changed + const categoryWasChanged = category.curr !== category.prev; + + if (categoryWasChanged) { + // Delete bookmark from old category + dispatch({ + type: ActionTypes.deleteBookmark, + payload: { + bookmarkId, + categoryId: category.prev, + }, + }); + + // Add bookmark to the new category + dispatch({ + type: ActionTypes.addBookmark, + payload: res.data.data, + }); + } else { + // Else update only name/url/icon + dispatch({ + type: ActionTypes.updateBookmark, + payload: res.data.data, + }); + } + } catch (err) { + console.log(err); + } + }; /** * SORT CATEGORIES @@ -284,16 +318,16 @@ export interface SortCategoriesAction { export const sortCategories = () => async (dispatch: Dispatch) => { try { - const res = await axios.get>('/api/config/useOrdering'); + const res = await axios.get>('/api/config'); dispatch({ type: ActionTypes.sortCategories, - payload: res.data.data.value - }) + payload: res.data.data.useOrdering, + }); } catch (err) { console.log(err); } -} +}; /** * REORDER CATEGORIES @@ -307,25 +341,31 @@ interface ReorderQuery { categories: { id: number; orderId: number; - }[] + }[]; } -export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => { - try { - const updateQuery: ReorderQuery = { categories: [] } +export const reorderCategories = + (categories: Category[]) => async (dispatch: Dispatch) => { + try { + const updateQuery: ReorderQuery = { categories: [] }; - categories.forEach((category, index) => updateQuery.categories.push({ - id: category.id, - orderId: index + 1 - })) + categories.forEach((category, index) => + updateQuery.categories.push({ + id: category.id, + orderId: index + 1, + }) + ); - await axios.put>('/api/categories/0/reorder', updateQuery); + await axios.put>( + '/api/categories/0/reorder', + updateQuery + ); - dispatch({ - type: ActionTypes.reorderCategories, - payload: categories - }) - } catch (err) { - console.log(err); - } -} \ No newline at end of file + dispatch({ + type: ActionTypes.reorderCategories, + payload: categories, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index 29c5186..8b1ef5a 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -3,16 +3,15 @@ import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; import { Config, ApiResponse, Query } from '../../interfaces'; import { CreateNotificationAction } from './notification'; -import { searchConfig } from '../../utility'; export interface GetConfigAction { type: ActionTypes.getConfig; - payload: Config[]; + payload: Config; } export const getConfig = () => async (dispatch: Dispatch) => { try { - const res = await axios.get>('/api/config'); + const res = await axios.get>('/api/config'); dispatch({ type: ActionTypes.getConfig, @@ -20,7 +19,7 @@ export const getConfig = () => async (dispatch: Dispatch) => { }); // Set custom page title if set - document.title = searchConfig('customTitle', 'Flame'); + document.title = res.data.data.customTitle; } catch (err) { console.log(err); } @@ -28,12 +27,12 @@ export const getConfig = () => async (dispatch: Dispatch) => { export interface UpdateConfigAction { type: ActionTypes.updateConfig; - payload: Config[]; + payload: Config; } export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { try { - const res = await axios.put>('/api/config', formData); + const res = await axios.put>('/api/config', formData); dispatch({ type: ActionTypes.createNotification, diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index ae2699e..c0ece13 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -1,15 +1,16 @@ import { ActionTypes, Action } from '../actions'; import { Config, Query } from '../../interfaces'; +import { configTemplate } from '../../utility'; export interface State { loading: boolean; - config: Config[]; + config: Config; customQueries: Query[]; } const initialState: State = { loading: true, - config: [], + config: configTemplate, customQueries: [], }; diff --git a/controllers/apps/docker/useDocker.js b/controllers/apps/docker/useDocker.js index fcc4379..88ecb3e 100644 --- a/controllers/apps/docker/useDocker.js +++ b/controllers/apps/docker/useDocker.js @@ -1,8 +1,8 @@ -const App = require('../../models/App'); +const App = require('../../../models/App'); const axios = require('axios'); -const Logger = require('../../utils/Logger'); +const Logger = require('../../../utils/Logger'); const logger = new Logger(); -const loadConfig = require('../../utils/loadConfig'); +const loadConfig = require('../../../utils/loadConfig'); const useDocker = async (apps) => { const { @@ -50,7 +50,7 @@ const useDocker = async (apps) => { for (const container of containers) { let labels = container.Labels; - // todo + // Traefik labels for URL configuration if (!('flame.url' in labels)) { for (const label of Object.keys(labels)) { if (/^traefik.*.frontend.rule/.test(label)) { diff --git a/controllers/bookmark.js b/controllers/bookmark.js deleted file mode 100644 index e745d4d..0000000 --- a/controllers/bookmark.js +++ /dev/null @@ -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: {}, - }); -}); diff --git a/controllers/bookmarks/createBookmark.js b/controllers/bookmarks/createBookmark.js new file mode 100644 index 0000000..2292c50 --- /dev/null +++ b/controllers/bookmarks/createBookmark.js @@ -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; diff --git a/controllers/bookmarks/deleteBookmark.js b/controllers/bookmarks/deleteBookmark.js new file mode 100644 index 0000000..c511a30 --- /dev/null +++ b/controllers/bookmarks/deleteBookmark.js @@ -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; diff --git a/controllers/bookmarks/getAllBookmarks.js b/controllers/bookmarks/getAllBookmarks.js new file mode 100644 index 0000000..c4d8dde --- /dev/null +++ b/controllers/bookmarks/getAllBookmarks.js @@ -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; diff --git a/controllers/bookmarks/getSingleBookmark.js b/controllers/bookmarks/getSingleBookmark.js new file mode 100644 index 0000000..18c0cbf --- /dev/null +++ b/controllers/bookmarks/getSingleBookmark.js @@ -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; diff --git a/controllers/bookmarks/index.js b/controllers/bookmarks/index.js new file mode 100644 index 0000000..f1ef588 --- /dev/null +++ b/controllers/bookmarks/index.js @@ -0,0 +1,7 @@ +module.exports = { + createBookmark: require('./createBookmark'), + getAllBookmarks: require('./getAllBookmarks'), + getSingleBookmark: require('./getSingleBookmark'), + updateBookmark: require('./updateBookmark'), + deleteBookmark: require('./deleteBookmark'), +}; diff --git a/controllers/bookmarks/updateBookmark.js b/controllers/bookmarks/updateBookmark.js new file mode 100644 index 0000000..778d2eb --- /dev/null +++ b/controllers/bookmarks/updateBookmark.js @@ -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; diff --git a/controllers/category.js b/controllers/category.js index 0f1af58..557c1a1 100644 --- a/controllers/category.js +++ b/controllers/category.js @@ -4,15 +4,13 @@ const Category = require('../models/Category'); const Bookmark = require('../models/Bookmark'); const Config = require('../models/Config'); const { Sequelize } = require('sequelize'); +const loadConfig = require('../utils/loadConfig'); // @desc Create new category // @route POST /api/categories // @access Public exports.createCategory = asyncWrapper(async (req, res, next) => { - // Get config from database - const pinCategories = await Config.findOne({ - where: { key: 'pinCategoriesByDefault' }, - }); + const { pinCategoriesByDefault: pinCategories } = await loadConfig(); let category; @@ -37,12 +35,8 @@ exports.createCategory = asyncWrapper(async (req, res, next) => { // @route GET /api/categories // @access Public exports.getCategories = asyncWrapper(async (req, res, next) => { - // Get config from database - const useOrdering = await Config.findOne({ - where: { key: 'useOrdering' }, - }); + const { useOrdering: orderType } = await loadConfig(); - const orderType = useOrdering ? useOrdering.value : 'createdAt'; let categories; if (orderType == 'name') { diff --git a/controllers/config.js b/controllers/config.js deleted file mode 100644 index e5290aa..0000000 --- a/controllers/config.js +++ /dev/null @@ -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: {}, - }); -}); diff --git a/controllers/config/getCSS.js b/controllers/config/getCSS.js new file mode 100644 index 0000000..db6b783 --- /dev/null +++ b/controllers/config/getCSS.js @@ -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; diff --git a/controllers/config/getConfig.js b/controllers/config/getConfig.js new file mode 100644 index 0000000..cb196f7 --- /dev/null +++ b/controllers/config/getConfig.js @@ -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; diff --git a/controllers/config/index.js b/controllers/config/index.js new file mode 100644 index 0000000..ae3c828 --- /dev/null +++ b/controllers/config/index.js @@ -0,0 +1,6 @@ +module.exports = { + getCSS: require('./getCSS'), + updateCSS: require('./updateCSS'), + getConfig: require('./getConfig'), + updateConfig: require('./updateConfig'), +}; diff --git a/controllers/config/updateCSS.js b/controllers/config/updateCSS.js new file mode 100644 index 0000000..4deea76 --- /dev/null +++ b/controllers/config/updateCSS.js @@ -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; diff --git a/controllers/config/updateConfig.js b/controllers/config/updateConfig.js new file mode 100644 index 0000000..722f334 --- /dev/null +++ b/controllers/config/updateConfig.js @@ -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; diff --git a/middleware/multer.js b/middleware/multer.js index bd493f5..806e5b4 100644 --- a/middleware/multer.js +++ b/middleware/multer.js @@ -11,7 +11,7 @@ const storage = multer.diskStorage({ }, filename: (req, file, cb) => { cb(null, Date.now() + '--' + file.originalname); - } + }, }); const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml']; diff --git a/routes/bookmark.js b/routes/bookmark.js index c594738..f7e541b 100644 --- a/routes/bookmark.js +++ b/routes/bookmark.js @@ -4,21 +4,18 @@ const upload = require('../middleware/multer'); const { createBookmark, - getBookmarks, - getBookmark, + getAllBookmarks, + getSingleBookmark, updateBookmark, - deleteBookmark -} = require('../controllers/bookmark'); + deleteBookmark, +} = require('../controllers/bookmarks'); -router - .route('/') - .post(upload, createBookmark) - .get(getBookmarks); +router.route('/').post(upload, createBookmark).get(getAllBookmarks); router .route('/:id') - .get(getBookmark) + .get(getSingleBookmark) .put(upload, updateBookmark) .delete(deleteBookmark); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/routes/config.js b/routes/config.js index 8c9ac15..fbb632f 100644 --- a/routes/config.js +++ b/routes/config.js @@ -2,20 +2,14 @@ const express = require('express'); const router = express.Router(); const { - createPair, - getAllPairs, - getSinglePair, - updateValue, - updateValues, - deletePair, - updateCss, - getCss, + getCSS, + updateCSS, + getConfig, + updateConfig, } = 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; From 4ef9652ede50e9ac5efa6ec7097eb8979409c1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 22 Oct 2021 15:51:11 +0200 Subject: [PATCH 7/8] Added option to change date formatting. Added shortcuts to clear search bar --- CHANGELOG.md | 3 ++ client/src/App.tsx | 2 +- .../src/components/Home/functions/dateTime.ts | 39 +++++++++++++++++-- client/src/components/SearchBar/SearchBar.tsx | 1 + .../Settings/OtherSettings/OtherSettings.tsx | 12 ++++++ client/src/interfaces/Config.ts | 1 + client/src/interfaces/Forms.ts | 1 + client/src/store/actions/config.ts | 6 +++ .../utility/templateObjects/configTemplate.ts | 1 + .../templateObjects/settingsTemplate.ts | 1 + controllers/category.js | 14 +++---- 11 files changed, 68 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b91cc2..06f83ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### v1.7.1 (TBA) - 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 diff --git a/client/src/App.tsx b/client/src/App.tsx index 9311b4b..3968bcd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,7 +16,7 @@ import Settings from './components/Settings/Settings'; import Bookmarks from './components/Bookmarks/Bookmarks'; import NotificationCenter from './components/NotificationCenter/NotificationCenter'; -// Get config pairs from database +// Load config store.dispatch(getConfig()); // Set theme diff --git a/client/src/components/Home/functions/dateTime.ts b/client/src/components/Home/functions/dateTime.ts index 44cc5e1..ddcfc70 100644 --- a/client/src/components/Home/functions/dateTime.ts +++ b/client/src/components/Home/functions/dateTime.ts @@ -1,8 +1,39 @@ export const dateTime = (): string => { - const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const days = [ + '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(); - return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`; -} \ No newline at end of file + 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()}`; + } +}; diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 85175ff..b6a981f 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -72,6 +72,7 @@ const SearchBar = (props: ComponentProps): JSX.Element => { type="text" className={classes.SearchBar} onKeyUp={(e) => searchHandler(e)} + onDoubleClick={clearSearch} /> ); diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 3d82fa4..6610b65 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -92,6 +92,18 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { onChange={(e) => inputChangeHandler(e)} /> + + + + {/* BEAHVIOR OPTIONS */} diff --git a/client/src/interfaces/Config.ts b/client/src/interfaces/Config.ts index d0152c5..1b60ca7 100644 --- a/client/src/interfaces/Config.ts +++ b/client/src/interfaces/Config.ts @@ -19,4 +19,5 @@ export interface Config { dockerHost: string; kubernetesApps: boolean; unpinStoppedApps: boolean; + useAmericanDate: boolean; } diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 9123d62..411ce90 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -25,4 +25,5 @@ export interface OtherSettingsForm { dockerHost: string; kubernetesApps: boolean; unpinStoppedApps: boolean; + useAmericanDate: boolean; } diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index 8b1ef5a..79bcebe 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -20,6 +20,9 @@ export const getConfig = () => async (dispatch: Dispatch) => { // Set custom page title if set document.title = res.data.data.customTitle; + + // Store settings for priority UI elements + localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`); } catch (err) { console.log(err); } @@ -46,6 +49,9 @@ export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { type: ActionTypes.updateConfig, payload: res.data.data, }); + + // Store settings for priority UI elements + localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`); } catch (err) { console.log(err); } diff --git a/client/src/utility/templateObjects/configTemplate.ts b/client/src/utility/templateObjects/configTemplate.ts index bbc7998..4d4843f 100644 --- a/client/src/utility/templateObjects/configTemplate.ts +++ b/client/src/utility/templateObjects/configTemplate.ts @@ -21,4 +21,5 @@ export const configTemplate: Config = { dockerHost: 'localhost', kubernetesApps: false, unpinStoppedApps: false, + useAmericanDate: false, }; diff --git a/client/src/utility/templateObjects/settingsTemplate.ts b/client/src/utility/templateObjects/settingsTemplate.ts index 674931b..05bc887 100644 --- a/client/src/utility/templateObjects/settingsTemplate.ts +++ b/client/src/utility/templateObjects/settingsTemplate.ts @@ -14,6 +14,7 @@ export const otherSettingsTemplate: OtherSettingsForm = { dockerHost: 'localhost', kubernetesApps: true, unpinStoppedApps: true, + useAmericanDate: false, }; export const weatherSettingsTemplate: WeatherForm = { diff --git a/controllers/category.js b/controllers/category.js index 557c1a1..d10183f 100644 --- a/controllers/category.js +++ b/controllers/category.js @@ -15,14 +15,12 @@ exports.createCategory = asyncWrapper(async (req, res, next) => { let category; if (pinCategories) { - if (parseInt(pinCategories.value)) { - category = await Category.create({ - ...req.body, - isPinned: true, - }); - } else { - category = await Category.create(req.body); - } + category = await Category.create({ + ...req.body, + isPinned: true, + }); + } else { + category = await Category.create(req.body); } res.status(201).json({ From 98924ac00689de2849a305285f226777194113cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 22 Oct 2021 16:10:38 +0200 Subject: [PATCH 8/8] Pushed version 1.7.1 --- .env | 2 +- CHANGELOG.md | 2 +- client/.env | 2 +- db/migrations/01_new-config.js | 8 ++------ 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.env b/.env index 1bb2edb..e2c26fc 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.7.0 \ No newline at end of file +VERSION=1.7.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f83ab..fc2dbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### v1.7.1 (TBA) +### 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)) diff --git a/client/.env b/client/.env index 6dbe18b..1511942 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.7.0 \ No newline at end of file +REACT_APP_VERSION=1.7.1 \ No newline at end of file diff --git a/db/migrations/01_new-config.js b/db/migrations/01_new-config.js index 2c42af7..6429e4f 100644 --- a/db/migrations/01_new-config.js +++ b/db/migrations/01_new-config.js @@ -1,5 +1,3 @@ -const { DataTypes } = require('sequelize'); -const { INTEGER, DATE, STRING, TINYINT, FLOAT, TEXT } = DataTypes; const { readFile, writeFile, copyFile } = require('fs/promises'); const Config = require('../../models/Config'); @@ -28,12 +26,10 @@ const up = async (query) => { const newConfig = JSON.stringify(parsedNewConfig); await writeFile('data/config.json', newConfig); - // await query.dropTable('config'); + await query.dropTable('config'); }; -const down = async (query) => { - // await query.dropTable('config'); -}; +const down = async (query) => {}; module.exports = { up,