From 1fbe0746a45ffde112e423c1088e4c14b4ce6165 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 28 Jul 2021 11:36:48 +0200 Subject: [PATCH 001/166] Fixed custom icons not updating --- CHANGELOG.md | 46 +++++++++++++++++++ .../src/components/Apps/AppCard/AppCard.tsx | 2 +- .../src/components/Apps/AppForm/AppForm.tsx | 30 ++++++++---- controllers/apps.js | 9 +++- package.json | 2 +- routes/apps.js | 2 +- 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..44a7ec8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +### v1.6 (2021-07-17) +- Added support for Steam URLs ([#62](https://github.com/pawelmalak/flame/issues/62)) +- Fixed bug with custom CSS not persisting ([#64](https://github.com/pawelmalak/flame/issues/64)) +- Added option to set default prefix for search bar ([#65](https://github.com/pawelmalak/flame/issues/65)) + +### v1.5 (2021-06-24) +- Added ability to set custom CSS from settings ([#8](https://github.com/pawelmalak/flame/issues/8) and [#17](https://github.com/pawelmalak/flame/issues/17)) (experimental) +- Added option to upload custom icons ([#12](https://github.com/pawelmalak/flame/issues/12)) +- Added option to open links in a new or the same tab ([#27](https://github.com/pawelmalak/flame/issues/27)) +- Added Search bar with support for 3 search engines and 4 services ([#44](https://github.com/pawelmalak/flame/issues/44)) +- Added option to hide applications and categories ([#48](https://github.com/pawelmalak/flame/issues/48)) +- Improved Logger + +### v1.4 (2021-06-18) +- Added more sorting options. User can now choose to sort apps and categories by name, creation time or to use custom order ([#13](https://github.com/pawelmalak/flame/issues/13)) +- Added reordering functionality. User can now set custom order for apps and categories from their 'edit tables' ([#13](https://github.com/pawelmalak/flame/issues/13)) +- Changed get all controllers for applications and categories to use case-insensitive ordering ([#36](https://github.com/pawelmalak/flame/issues/36)) +- New apps will be placed correctly in the array depending on used sorting settings ([#37](https://github.com/pawelmalak/flame/issues/37)) +- Added app version to settings with option to check for updates manually ([#38](https://github.com/pawelmalak/flame/issues/38)) +- Added update check on app start ([#38](https://github.com/pawelmalak/flame/issues/38)) +- Fixed bug with decimal input values in Safari browser ([#40](https://github.com/pawelmalak/flame/issues/40)) + +### v1.3 (2021-06-14) +- Added reverse proxy support ([#23](https://github.com/pawelmalak/flame/issues/23) and [#24](https://github.com/pawelmalak/flame/issues/24)) +- Added support for more url formats ([#26](https://github.com/pawelmalak/flame/issues/26)) +- Added ability to hide main header ([#28](https://github.com/pawelmalak/flame/issues/28)) +- Fixed settings not being synchronized ([#29](https://github.com/pawelmalak/flame/issues/29)) +- Added auto-refresh for greeting and date ([#34](https://github.com/pawelmalak/flame/issues/34)) + +### v1.2 (2021-06-10) +- Added simple check to the weather module settings to inform user if the api key is missing ([#2](https://github.com/pawelmalak/flame/issues/2)) +- Added ability to set optional icons to the bookmarks ([#7](https://github.com/pawelmalak/flame/issues/7)) +- Added option to pin new applications and categories to the homescreen by default ([#11](https://github.com/pawelmalak/flame/issues/11)) +- Added background highlight while hovering over application card ([#15](https://github.com/pawelmalak/flame/issues/15)) +- Created CRON job to clear old weather data from the database ([#16](https://github.com/pawelmalak/flame/issues/16)) +- Added proxy for websocket instead of using hard coded host ([#18](https://github.com/pawelmalak/flame/issues/18)) +- Fixed bug with overwriting opened tabs ([#20](https://github.com/pawelmalak/flame/issues/20)) + +### v1.1 (2021-06-09) +- Added custom favicon and changed page title ([#3](https://github.com/pawelmalak/flame/issues/3)) +- Added functionality to set custom page title ([#3](https://github.com/pawelmalak/flame/issues/3)) +- Changed messages on the homescreen when there are apps/bookmarks created but not pinned to the homescreen ([#4](https://github.com/pawelmalak/flame/issues/4)) +- Added 'warnings' to apps and bookmarks forms about supported url formats ([#5](https://github.com/pawelmalak/flame/issues/5)) + +### v1.0 (2021-06-08) +Initial release of Flame - self-hosted startpage using Node.js on backend and React on frontend. \ No newline at end of file diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 43d7b72..79ad3d8 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -21,7 +21,7 @@ const AppCard = (props: ComponentProps): JSX.Element => { className={classes.AppCard} >
- {(/.(jpeg|jpg|png)$/).test(props.app.icon) + {(/.(jpeg|jpg|png)$/i).test(props.app.icon) ? {`${props.app.name} void; addApp: (formData: NewApp | FormData) => any; - updateApp: (id: number, formData: NewApp) => any; + updateApp: (id: number, formData: NewApp | FormData) => any; app?: App; } @@ -66,21 +65,32 @@ const AppForm = (props: ComponentProps): JSX.Element => { const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); + const createFormData = (): FormData => { + const data = new FormData(); + if (customIcon) { + data.append('icon', customIcon); + } + data.append('name', formData.name); + data.append('url', formData.url); + + return data; + } + if (!props.app) { if (customIcon) { - const data = new FormData(); - data.append('icon', customIcon); - - data.append('name', formData.name); - data.append('url', formData.url); - + const data = createFormData(); props.addApp(data); } else { props.addApp(formData); } } else { - props.updateApp(props.app.id, formData); - props.modalHandler(); + if (customIcon) { + const data = createFormData(); + props.updateApp(props.app.id, data); + } else { + props.updateApp(props.app.id, formData); + props.modalHandler(); + } } setFormData({ diff --git a/controllers/apps.js b/controllers/apps.js index 238c66b..92c1736 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -20,7 +20,6 @@ exports.createApp = asyncWrapper(async (req, res, next) => { _body.icon = req.file.filename; } - if (pinApps) { if (parseInt(pinApps.value)) { app = await App.create({ @@ -96,7 +95,13 @@ exports.updateApp = asyncWrapper(async (req, res, next) => { return next(new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)); } - app = await app.update({ ...req.body }); + let _body = { ...req.body }; + + if (req.file) { + _body.icon = req.file.filename; + } + + app = await app.update(_body); res.status(200).json({ success: true, diff --git a/package.json b/package.json index 3150454..2716484 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "node server.js", - "init-server": "echo Instaling server dependencies && npm install", + "init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css", "init-client": "cd client && echo Instaling client dependencies && npm install", "dev-init": "npm run init-server && npm run init-client", "dev-server": "nodemon server.js", diff --git a/routes/apps.js b/routes/apps.js index 091550c..37c0286 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -19,7 +19,7 @@ router router .route('/:id') .get(getApp) - .put(updateApp) + .put(upload, updateApp) .delete(deleteApp); router From a5d6cf04cff771073468a6c0ee2c2ca8c659f2ae Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 28 Jul 2021 12:36:03 +0200 Subject: [PATCH 002/166] Custom icons for bookmarks --- CHANGELOG.md | 4 + client/.env | 2 +- .../src/components/Apps/AppForm/AppForm.tsx | 13 +- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 9 +- .../BookmarkForm/BookmarkForm.module.css | 7 + .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 130 ++++++++++++++---- client/src/store/actions/bookmark.ts | 23 ++-- controllers/bookmark.js | 24 +++- routes/bookmark.js | 5 +- 9 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 client/src/components/Bookmarks/BookmarkForm/BookmarkForm.module.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a7ec8..d1ed220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### v1.6.1 (2021-07-28) +- Added option to upload custom icons for bookmarks ([#52](https://github.com/pawelmalak/flame/issues/52)) +- Fixed custom icons not updating ([#58](https://github.com/pawelmalak/flame/issues/58)) + ### v1.6 (2021-07-17) - Added support for Steam URLs ([#62](https://github.com/pawelmalak/flame/issues/62)) - Fixed bug with custom CSS not persisting ([#64](https://github.com/pawelmalak/flame/issues/64)) diff --git a/client/.env b/client/.env index 036129f..f56a185 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.0 \ No newline at end of file +REACT_APP_VERSION=1.6.1 \ No newline at end of file diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index d8f3923..72d8db2 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, ChangeEvent, SyntheticEvent } from 'react'; +import { useState, useEffect, ChangeEvent, SyntheticEvent } from 'react'; import { connect } from 'react-redux'; import { addApp, updateApp } from '../../../store/actions'; import { App, NewApp } from '../../../interfaces'; @@ -25,14 +25,6 @@ const AppForm = (props: ComponentProps): JSX.Element => { icon: '' }); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, [inputRef]) - useEffect(() => { if (props.app) { setFormData({ @@ -98,6 +90,8 @@ const AppForm = (props: ComponentProps): JSX.Element => { url: '', icon: '' }) + + setCustomIcon(null); } return ( @@ -115,7 +109,6 @@ const AppForm = (props: ComponentProps): JSX.Element => { required value={formData.name} onChange={(e) => inputChangeHandler(e)} - ref={inputRef} /> diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index f2535b5..fe2198b 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -24,7 +24,14 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { key={`bookmark-${bookmark.id}`}> {bookmark.icon && (
- + {(/.(jpeg|jpg|png)$/i).test(bookmark.icon) + ? {`${bookmark.name} + : + }
)} {bookmark.name} diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.module.css b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.module.css new file mode 100644 index 0000000..66b15a0 --- /dev/null +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.module.css @@ -0,0 +1,7 @@ +.Switch { + text-decoration: underline; +} + +.Switch:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index eb83013..67059ae 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -7,6 +7,7 @@ import { Bookmark, Category, GlobalState, NewBookmark, NewCategory, NewNotificat import { ContentType } from '../Bookmarks'; import { getCategories, addCategory, addBookmark, updateCategory, updateBookmark, createNotification } from '../../../store/actions'; import Button from '../../UI/Buttons/Button/Button'; +import classes from './BookmarkForm.module.css'; interface ComponentProps { modalHandler: () => void; @@ -15,13 +16,22 @@ interface ComponentProps { category?: Category; bookmark?: Bookmark; addCategory: (formData: NewCategory) => void; - addBookmark: (formData: NewBookmark) => void; + addBookmark: (formData: NewBookmark | FormData) => void; updateCategory: (id: number, formData: NewCategory) => void; - updateBookmark: (id: number, formData: NewBookmark, previousCategoryId: number) => void; + updateBookmark: ( + id: number, + formData: NewBookmark | FormData, + category: { + prev: number, + curr: number + } + ) => void; createNotification: (notification: NewNotification) => void; } const BookmarkForm = (props: ComponentProps): JSX.Element => { + const [useCustomIcon, toggleUseCustomIcon] = useState(false); + const [customIcon, setCustomIcon] = useState(null); const [categoryName, setCategoryName] = useState({ name: '' }) @@ -64,6 +74,18 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); + const createFormData = (): FormData => { + const data = new FormData(); + if (customIcon) { + data.append('icon', customIcon); + } + data.append('name', formData.name); + data.append('url', formData.url); + data.append('categoryId', `${formData.categoryId}`); + + return data; + } + if (!props.category && !props.bookmark) { // Add new if (props.contentType === ContentType.category) { @@ -79,14 +101,22 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { }) return; } - - props.addBookmark(formData); + + if (customIcon) { + const data = createFormData(); + props.addBookmark(data); + } else { + props.addBookmark(formData); + } + setFormData({ name: '', url: '', categoryId: formData.categoryId, icon: '' }) + + setCustomIcon(null) } } else { // Update @@ -96,13 +126,35 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { setCategoryName({ name: '' }); } else if (props.contentType === ContentType.bookmark && props.bookmark) { // Update bookmark - props.updateBookmark(props.bookmark.id, formData, props.bookmark.categoryId); + if (customIcon) { + const data = createFormData(); + props.updateBookmark( + props.bookmark.id, + data, + { + prev: props.bookmark.categoryId, + curr: formData.categoryId + } + ) + } else { + props.updateBookmark( + props.bookmark.id, + formData, + { + prev: props.bookmark.categoryId, + curr: formData.categoryId + } + ); + } + setFormData({ name: '', url: '', categoryId: -1, icon: '' }) + + setCustomIcon(null) } props.modalHandler(); @@ -123,6 +175,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { }) } + const fileChangeHandler = (e: ChangeEvent): void => { + if (e.target.files) { + setCustomIcon(e.target.files[0]); + } + } + let button = if (!props.category && !props.bookmark) { @@ -216,25 +274,49 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { })}
- - - inputChangeHandler(e)} - /> - - Use icon name from MDI. - - {' '}Click here for reference - - - + {!useCustomIcon + // mdi + ? ( + + inputChangeHandler(e)} + /> + + Use icon name from MDI. + + {' '}Click here for reference + + + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to custom icon upload + + ) + // custom + : ( + + fileChangeHandler(e)} + accept='.jpg,.jpeg,.png' + /> + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to MDI + + ) + } ) } diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index 0398bbb..8707062 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -69,15 +69,15 @@ export interface AddBookmarkAction { payload: Bookmark } -export const addBookmark = (formData: NewBookmark) => async (dispatch: Dispatch) => { +export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.post>('/api/bookmarks', formData); - + console.log(res.data.data) dispatch({ type: ActionTypes.createNotification, payload: { title: 'Success', - message: `Bookmark ${formData.name} created` + message: `Bookmark created` } }) @@ -225,7 +225,14 @@ export interface UpdateBookmarkAction { payload: Bookmark } -export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previousCategoryId: number) => async (dispatch: Dispatch) => { +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); @@ -233,12 +240,12 @@ export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previo type: ActionTypes.createNotification, payload: { title: 'Success', - message: `Bookmark ${formData.name} updated` + message: `Bookmark updated` } }) // Check if category was changed - const categoryWasChanged = formData.categoryId !== previousCategoryId; + const categoryWasChanged = category.curr !== category.prev; if (categoryWasChanged) { // Delete bookmark from old category @@ -246,7 +253,7 @@ export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previo type: ActionTypes.deleteBookmark, payload: { bookmarkId, - categoryId: previousCategoryId + categoryId: category.prev } }) @@ -256,7 +263,7 @@ export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previo payload: res.data.data }) } else { - // Else update only name/url + // Else update only name/url/icon dispatch({ type: ActionTypes.updateBookmark, payload: res.data.data diff --git a/controllers/bookmark.js b/controllers/bookmark.js index 08b2fca..8077a8c 100644 --- a/controllers/bookmark.js +++ b/controllers/bookmark.js @@ -7,7 +7,18 @@ const { Sequelize } = require('sequelize'); // @route POST /api/bookmarks // @access Public exports.createBookmark = asyncWrapper(async (req, res, next) => { - const bookmark = await Bookmark.create(req.body); + 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, @@ -59,7 +70,16 @@ exports.updateBookmark = asyncWrapper(async (req, res, next) => { return next(new ErrorResponse(`Bookmark with id of ${req.params.id} was not found`, 404)); } - bookmark = await bookmark.update({ ...req.body }); + 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, diff --git a/routes/bookmark.js b/routes/bookmark.js index f0d62f4..c594738 100644 --- a/routes/bookmark.js +++ b/routes/bookmark.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const upload = require('../middleware/multer'); const { createBookmark, @@ -11,13 +12,13 @@ const { router .route('/') - .post(createBookmark) + .post(upload, createBookmark) .get(getBookmarks); router .route('/:id') .get(getBookmark) - .put(updateBookmark) + .put(upload, updateBookmark) .delete(deleteBookmark); module.exports = router; \ No newline at end of file From d39eda49de3d3c7f2399f5bba80274cb3a6dc05d Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 28 Jul 2021 12:52:30 +0200 Subject: [PATCH 003/166] Added changelog. Added curl to Docker image --- .dockerignore | 3 ++- .gitignore | 3 ++- CHANGELOG.md | 1 + Dockerfile | 2 +- Dockerfile.multiarch | 2 +- client/src/components/Settings/AppDetails/AppDetails.tsx | 9 +++++++++ client/src/store/actions/bookmark.ts | 2 +- 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.dockerignore b/.dockerignore index 5fcee18..da9bc10 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ node_modules github -public \ No newline at end of file +public +build.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2955045..98ec862 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules data -public \ No newline at end of file +public +build.sh \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ed220..56860ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### v1.6.1 (2021-07-28) - Added option to upload custom icons for bookmarks ([#52](https://github.com/pawelmalak/flame/issues/52)) - Fixed custom icons not updating ([#58](https://github.com/pawelmalak/flame/issues/58)) +- Added changelog file ### v1.6 (2021-07-17) - Added support for Steam URLs ([#62](https://github.com/pawelmalak/flame/issues/62)) diff --git a/Dockerfile b/Dockerfile index 8016d8e..fed0789 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:14-alpine -RUN apk update && apk add --no-cache nano +RUN apk update && apk add --no-cache nano curl WORKDIR /app diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index 808b815..20ff6c2 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -1,6 +1,6 @@ FROM node:14-alpine -RUN apk update && apk add --no-cache nano +RUN apk update && apk add --no-cache nano curl WORKDIR /app diff --git a/client/src/components/Settings/AppDetails/AppDetails.tsx b/client/src/components/Settings/AppDetails/AppDetails.tsx index 90fe2fb..50fd37f 100644 --- a/client/src/components/Settings/AppDetails/AppDetails.tsx +++ b/client/src/components/Settings/AppDetails/AppDetails.tsx @@ -17,6 +17,15 @@ const AppDetails = (): JSX.Element => { {' '} version {process.env.REACT_APP_VERSION}

+

+ See changelog {' '} + + here + +

) diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index 8707062..b4b5831 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -72,7 +72,7 @@ export interface AddBookmarkAction { export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.post>('/api/bookmarks', formData); - console.log(res.data.data) + dispatch({ type: ActionTypes.createNotification, payload: { From 4b42f991f8073af18577c8a1cdfb4803c2cfebaf Mon Sep 17 00:00:00 2001 From: Bhanu Date: Tue, 3 Aug 2021 11:51:54 +0530 Subject: [PATCH 004/166] Use correct changelog link current link in release 1.6.1 points to an incorrect url that returns 404 --- client/src/components/Settings/AppDetails/AppDetails.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Settings/AppDetails/AppDetails.tsx b/client/src/components/Settings/AppDetails/AppDetails.tsx index 50fd37f..109053a 100644 --- a/client/src/components/Settings/AppDetails/AppDetails.tsx +++ b/client/src/components/Settings/AppDetails/AppDetails.tsx @@ -20,7 +20,7 @@ const AppDetails = (): JSX.Element => {

See changelog {' '} here @@ -31,4 +31,4 @@ const AppDetails = (): JSX.Element => { ) } -export default AppDetails; \ No newline at end of file +export default AppDetails; From b94df53267ecb534fce5468d1529aae4972ac82a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 3 Aug 2021 10:22:55 +0200 Subject: [PATCH 005/166] github directory name changed --- {github => .github}/_apps.png | Bin {github => .github}/_bookmarks.png | Bin {github => .github}/_home.png | Bin {github => .github}/_themes.png | Bin README.md | 43 +++++++++++++++++++++-------- 5 files changed, 31 insertions(+), 12 deletions(-) rename {github => .github}/_apps.png (100%) rename {github => .github}/_bookmarks.png (100%) rename {github => .github}/_home.png (100%) rename {github => .github}/_themes.png (100%) diff --git a/github/_apps.png b/.github/_apps.png similarity index 100% rename from github/_apps.png rename to .github/_apps.png diff --git a/github/_bookmarks.png b/.github/_bookmarks.png similarity index 100% rename from github/_bookmarks.png rename to .github/_bookmarks.png diff --git a/github/_home.png b/.github/_home.png similarity index 100% rename from github/_home.png rename to .github/_home.png diff --git a/github/_themes.png b/.github/_themes.png similarity index 100% rename from github/_themes.png rename to .github/_themes.png diff --git a/README.md b/README.md index 8e27c86..5c1abe2 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,26 @@ [![Node Badge](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://shields.io/) [![React Badge](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://shields.io/) -![Homescreen screenshot](./github/_home.png) +![Homescreen screenshot](./.github/_home.png) ## Description + Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own appliaction hub in no time - no file editing necessary. ## Technology + - Backend - Node.js + Express - Sequelize ORM + SQLite - Frontend - - React + - React - Redux - TypeScript - Deployment - Docker ## Development + ```sh # clone repository git clone https://github.com/pawelmalak/flame @@ -34,13 +37,14 @@ npm run dev-init npm run dev ``` -## Installation +## Installation ### With Docker (recommended) [Docker Hub](https://hub.docker.com/r/pawelmalak/flame) #### Building images + ```sh # build image for amd64 only docker build -t flame . @@ -54,14 +58,16 @@ docker buildx build \ ``` #### Deployment + ```sh # run container docker run -p 5005:5005 -v /path/to/data:/app/data flame ``` #### Docker-Compose + ```yaml -version: "2.1" +version: '2.1' services: flame: image: pawelmalak/flame:latest @@ -74,46 +80,54 @@ services: ``` ### Without Docker + Follow instructions from wiki: [Installation without Docker](https://github.com/pawelmalak/flame/wiki/Installation-without-docker) ## Functionality + - Applications - Create, update, delete and organize applications using GUI - Pin your favourite apps to homescreen -![Homescreen screenshot](./github/_apps.png) +![Homescreen screenshot](./.github/_apps.png) - Bookmarks - Create, update, delete and organize bookmarks and categories using GUI - Pin your favourite categories to homescreen -![Homescreen screenshot](./github/_bookmarks.png) +![Homescreen screenshot](./.github/_bookmarks.png) - Weather + - Get current temperature, cloud coverage and weather status with animated icons - Themes - - Customize your page by choosing from 12 color themes + - Customize your page by choosing from 12 color themes -![Homescreen screenshot](./github/_themes.png) +![Homescreen screenshot](./.github/_themes.png) ## Usage + ### Search bar + #### Searching + To use search bar you need to type your search query with selected prefix. For example, to search for "what is docker" using google search you would type: `/g what is docker`. > You can change where to open search results (same/new tab) in the settings #### Supported search engines + | Name | Prefix | Search URL | -|------------|--------|-------------------------------------| +| ---------- | ------ | ----------------------------------- | | Disroot | /ds | http://search.disroot.org/search?q= | | DuckDuckGo | /d | https://duckduckgo.com/?q= | | Google | /g | https://www.google.com/search?q= | #### Supported services + | Name | Prefix | Search URL | -|--------------------|--------|-----------------------------------------------| +| ------------------ | ------ | --------------------------------------------- | | IMDb | /im | https://www.imdb.com/find?q= | | Reddit | /r | https://www.reddit.com/search?q= | | Spotify | /sp | https://open.spotify.com/search/ | @@ -121,13 +135,16 @@ To use search bar you need to type your search query with selected prefix. For e | Youtube | /yt | https://www.youtube.com/results?search_query= | ### Setting up weather module + 1. Obtain API Key from [Weather API](https://www.weatherapi.com/pricing.aspx). > Free plan allows for 1M calls per month. Flame is making less then 3K API calls per month. 2. Get lat/long for your location. You can get them from [latlong.net](https://www.latlong.net/convert-address-to-lat-long.html). 3. Enter and save data. Weather widget will now update and should be visible on Home page. ### Supported URL formats for applications and bookmarks + #### Rules + - URL starts with `http://` - Format: `http://www.domain.com`, `http://domain.com` - Redirect: `{dest}` @@ -139,11 +156,13 @@ To use search bar you need to type your search query with selected prefix. For e - Redirect: `http://{dest}` ### Custom CSS + > This is an experimental feature. Its behaviour might change in the future. -> -Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) +> +> Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) ## Support + If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link: [![PayPal Badge](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/pawelmalak) From b53509aa691d5889fe8cd9dd4afba227c59cc653 Mon Sep 17 00:00:00 2001 From: Pablo Ruiz Date: Wed, 4 Aug 2021 22:19:35 +0200 Subject: [PATCH 006/166] docker api --- README.md | 15 +++++ .../Settings/OtherSettings/OtherSettings.tsx | 33 ++++++++++- client/src/interfaces/Forms.ts | 2 + controllers/apps.js | 55 ++++++++++++++++++- utils/initialConfig.json | 8 +++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c1abe2..4074c61 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ services: container_name: flame volumes: - :/app/data + - /var/run/docker.sock:/var/sock/docker.sock # Docker socket ports: - 5005:5005 restart: unless-stopped @@ -155,6 +156,20 @@ To use search bar you need to type your search query with selected prefix. For e - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port` - Redirect: `http://{dest}` +### Docker integration + +In order to use the Docker integration, each container must have the following labels: + +```yml +labels: + - flame.type=application # "app" works too + - flame.name=My container + - flame.url=https://example.com + - flame.icon=icon-name # Optional, default is "docker" +``` + +And you must have activated the Docker sync option in the settings panel. + ### Custom CSS > This is an experimental feature. Its behaviour might change in the future. diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 199e9ff..cdf5302 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -40,7 +40,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { useOrdering: 'createdAt', appsSameTab: 0, bookmarksSameTab: 0, - searchSameTab: 0 + searchSameTab: 0, + dockerApps:1, + unpinStoppedApps: 1 }) // Get config @@ -57,7 +59,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { useOrdering: searchConfig('useOrdering', 'createdAt'), appsSameTab: searchConfig('appsSameTab', 0), bookmarksSameTab: searchConfig('bookmarksSameTab', 0), - searchSameTab: searchConfig('searchSameTab', 0) + searchSameTab: searchConfig('searchSameTab', 0), + dockerApps: searchConfig('dockerApps', 1), + unpinStoppedApps: searchConfig('unpinStoppedApps', 1) }) }, [props.loading]); @@ -243,6 +247,31 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { +

Docker

+ + + + + + + + ) diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 139a638..8717d03 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -18,4 +18,6 @@ export interface SettingsForm { appsSameTab: number; bookmarksSameTab: number; searchSameTab: number; + dockerApps: number; + unpinStoppedApps: number; } diff --git a/controllers/apps.js b/controllers/apps.js index 92c1736..128e657 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -3,6 +3,7 @@ const ErrorResponse = require('../utils/ErrorResponse'); const App = require('../models/App'); const Config = require('../models/Config'); const { Sequelize } = require('sequelize'); +const axios = require('axios'); // @desc Create new app // @route POST /api/apps @@ -45,10 +46,61 @@ exports.getApps = asyncWrapper(async (req, res, next) => { const useOrdering = await Config.findOne({ where: { key: 'useOrdering' } }); + const useDockerApi = await Config.findOne({ + where: { key: 'dockerApps' } + }); + const unpinStoppedApps = await Config.findOne({ + where: { key: 'unpinStoppedApps' } + }); const orderType = useOrdering ? useOrdering.value : 'createdAt'; let apps; + + if (useDockerApi && useDockerApi.value==1) { + let {data:containers} = await axios.get('http://localhost/containers/json?{"status":["running"]}', { + socketPath: '/var/run/docker.sock' + }); + + if (containers) { + apps = await App.findAll({ + order: [[ orderType, 'ASC' ]] + }); + + containers = containers.filter((e) => Object.keys(e.Labels).length !== 0); + const dockerApps = []; + for (const container of containers) { + const labels = container.Labels; + + if ('flame.name' in labels && 'flame.url' in labels && (labels['flame.type']==='application' || labels['flame.type']==='app')) { + dockerApps.push({ + name: labels['flame.name'], + url: labels['flame.url'], + icon: labels['flame.icon'] || '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]; + 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' ]] @@ -59,7 +111,8 @@ exports.getApps = asyncWrapper(async (req, res, next) => { }); } - res.status(200).json({ + // Set header to fetch containers info every time + res.status(200).setHeader('Cache-Control','no-store').json({ success: true, data: apps }) diff --git a/utils/initialConfig.json b/utils/initialConfig.json index 39d2e08..0d0613c 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -63,6 +63,14 @@ { "key": "defaultSearchProvider", "value": "d" + }, + { + "key": "dockerApps", + "value": true + }, + { + "key": "unpinStoppedApps", + "value": true } ] } From 39349dded1ad44a3e5662b46d1477e696cf31258 Mon Sep 17 00:00:00 2001 From: Pablo Ruiz Date: Thu, 5 Aug 2021 08:56:02 +0200 Subject: [PATCH 007/166] fix optional docker.sock mount --- README.md | 2 +- controllers/apps.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4074c61..c15bf65 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ services: container_name: flame volumes: - :/app/data - - /var/run/docker.sock:/var/sock/docker.sock # Docker socket + - /var/run/docker.sock:/var/sock/docker.sock # optional but required for Docker integration feature ports: - 5005:5005 restart: unless-stopped diff --git a/controllers/apps.js b/controllers/apps.js index 128e657..7073780 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -4,6 +4,8 @@ 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(); // @desc Create new app // @route POST /api/apps @@ -58,9 +60,14 @@ exports.getApps = asyncWrapper(async (req, res, next) => { if (useDockerApi && useDockerApi.value==1) { - let {data:containers} = await axios.get('http://localhost/containers/json?{"status":["running"]}', { + let containers = null; + + try { + let {data} = await axios.get('http://localhost/containers/json?{"status":["running"]}', { socketPath: '/var/run/docker.sock' }); + containers = data; + } catch{logger.log("Can't connect to the docker socket","ERROR")} if (containers) { apps = await App.findAll({ @@ -72,7 +79,7 @@ exports.getApps = asyncWrapper(async (req, res, next) => { for (const container of containers) { const labels = container.Labels; - if ('flame.name' in labels && 'flame.url' in labels && (labels['flame.type']==='application' || labels['flame.type']==='app')) { + if ('flame.name' in labels && 'flame.url' in labels && /^app/.test(labels['flame.type'])) { dockerApps.push({ name: labels['flame.name'], url: labels['flame.url'], From a01661d0d5e0c736555b897c86328f2cf91dbed3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 6 Aug 2021 10:36:05 +0200 Subject: [PATCH 008/166] Pushed version 1.6.2. Small formatting fixes --- .prettierignore | 1 + CHANGELOG.md | 4 + client/.env | 2 +- .../Settings/OtherSettings/OtherSettings.tsx | 101 +++++++++++------- controllers/apps.js | 89 ++++++++------- utils/initialConfig.json | 4 +- 6 files changed, 123 insertions(+), 78 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2e1fa2d --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 56860ce..e6df0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### v1.6.2 (2021-08-06) +- Fixed changelog link +- Added support for Docker API ([#14](https://github.com/pawelmalak/flame/issues/14)) + ### v1.6.1 (2021-07-28) - Added option to upload custom icons for bookmarks ([#52](https://github.com/pawelmalak/flame/issues/52)) - Fixed custom icons not updating ([#58](https://github.com/pawelmalak/flame/issues/58)) diff --git a/client/.env b/client/.env index f56a185..0c25886 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.1 \ No newline at end of file +REACT_APP_VERSION=1.6.2 \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index cdf5302..80b0c0b 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -2,10 +2,20 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; // Redux import { connect } from 'react-redux'; -import { createNotification, updateConfig, sortApps, sortCategories } from '../../../store/actions'; +import { + createNotification, + updateConfig, + sortApps, + sortCategories +} from '../../../store/actions'; // Typescript -import { GlobalState, NewNotification, Query, SettingsForm } from '../../../interfaces'; +import { + GlobalState, + NewNotification, + Query, + SettingsForm +} from '../../../interfaces'; // UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; @@ -41,9 +51,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { appsSameTab: 0, bookmarksSameTab: 0, searchSameTab: 0, - dockerApps:1, + dockerApps: 1, unpinStoppedApps: 1 - }) + }); // Get config useEffect(() => { @@ -60,9 +70,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { appsSameTab: searchConfig('appsSameTab', 0), bookmarksSameTab: searchConfig('bookmarksSameTab', 0), searchSameTab: searchConfig('searchSameTab', 0), - dockerApps: searchConfig('dockerApps', 1), - unpinStoppedApps: searchConfig('unpinStoppedApps', 1) - }) + dockerApps: searchConfig('dockerApps', 0), + unpinStoppedApps: searchConfig('unpinStoppedApps', 0) + }); }, [props.loading]); // Form handler @@ -78,10 +88,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { // Sort apps and categories with new settings props.sortApps(); props.sortCategories(); - } + }; // Input handler - const inputChangeHandler = (e: ChangeEvent, isNumber?: boolean) => { + const inputChangeHandler = ( + e: ChangeEvent, + isNumber?: boolean + ) => { let value: string | number = e.target.value; if (isNumber) { @@ -91,11 +104,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { setFormData({ ...formData, [e.target.name]: value - }) - } + }); + }; return ( -
formSubmitHandler(e)}> + formSubmitHandler(e)}> {/* OTHER OPTIONS */}

Miscellaneous

@@ -106,31 +119,35 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { name='customTitle' placeholder='Flame' value={formData.customTitle} - onChange={(e) => inputChangeHandler(e)} + onChange={e => inputChangeHandler(e)} /> {/* BEAHVIOR OPTIONS */}

App Behavior

- + - + - + + + {/* DOCKER SETTINGS */}

Docker

@@ -254,40 +277,42 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { id='dockerApps' name='dockerApps' value={formData.dockerApps} - onChange={(e) => inputChangeHandler(e, true)} + onChange={e => inputChangeHandler(e, true)} > - + - +
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { loading: state.config.loading - } -} + }; +}; const actions = { createNotification, updateConfig, sortApps, sortCategories -} +}; -export default connect(mapStateToProps, actions)(OtherSettings); \ No newline at end of file +export default connect(mapStateToProps, actions)(OtherSettings); diff --git a/controllers/apps.js b/controllers/apps.js index 7073780..e4fa1bc 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -28,7 +28,7 @@ exports.createApp = asyncWrapper(async (req, res, next) => { app = await App.create({ ..._body, isPinned: true - }) + }); } else { app = await App.create(req.body); } @@ -37,8 +37,8 @@ exports.createApp = asyncWrapper(async (req, res, next) => { res.status(201).json({ success: true, data: app - }) -}) + }); +}); // @desc Get all apps // @route GET /api/apps @@ -58,37 +58,45 @@ exports.getApps = asyncWrapper(async (req, res, next) => { const orderType = useOrdering ? useOrdering.value : 'createdAt'; let apps; - - if (useDockerApi && useDockerApi.value==1) { + if (useDockerApi && useDockerApi.value == 1) { let containers = null; try { - let {data} = await axios.get('http://localhost/containers/json?{"status":["running"]}', { - socketPath: '/var/run/docker.sock' - }); - containers = data; - } catch{logger.log("Can't connect to the docker socket","ERROR")} + let { data } = await axios.get( + 'http://localhost/containers/json?{"status":["running"]}', + { + socketPath: '/var/run/docker.sock' + } + ); + containers = data; + } catch { + logger.log("Can't connect to the docker socket", 'ERROR'); + } if (containers) { apps = await App.findAll({ - order: [[ orderType, 'ASC' ]] + order: [[orderType, 'ASC']] }); - containers = containers.filter((e) => Object.keys(e.Labels).length !== 0); + containers = containers.filter(e => Object.keys(e.Labels).length !== 0); const dockerApps = []; for (const container of containers) { const labels = container.Labels; - if ('flame.name' in labels && 'flame.url' in labels && /^app/.test(labels['flame.type'])) { + if ( + 'flame.name' in labels && + 'flame.url' in labels && + /^app/.test(labels['flame.type']) + ) { dockerApps.push({ name: labels['flame.name'], url: labels['flame.url'], icon: labels['flame.icon'] || 'docker' - }) + }); } } - if (unpinStoppedApps && unpinStoppedApps.value==1) { + if (unpinStoppedApps && unpinStoppedApps.value == 1) { for (const app of apps) { await app.update({ isPinned: false }); } @@ -97,12 +105,12 @@ exports.getApps = asyncWrapper(async (req, res, next) => { for (const item of dockerApps) { if (apps.some(app => app.name === item.name)) { const app = apps.filter(e => e.name === item.name)[0]; - await app.update({ ...item,isPinned: true }); + await app.update({ ...item, isPinned: true }); } else { await App.create({ ...item, isPinned: true - }) + }); } } } @@ -110,20 +118,20 @@ exports.getApps = asyncWrapper(async (req, res, next) => { if (orderType == 'name') { apps = await App.findAll({ - order: [[ Sequelize.fn('lower', Sequelize.col('name')), 'ASC' ]] + order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']] }); } else { apps = await App.findAll({ - order: [[ orderType, 'ASC' ]] + order: [[orderType, 'ASC']] }); } // Set header to fetch containers info every time - res.status(200).setHeader('Cache-Control','no-store').json({ + res.status(200).setHeader('Cache-Control', 'no-store').json({ success: true, data: apps - }) -}) + }); +}); // @desc Get single app // @route GET /api/apps/:id @@ -134,14 +142,16 @@ exports.getApp = asyncWrapper(async (req, res, next) => { }); if (!app) { - return next(new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)); + 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 @@ -152,7 +162,9 @@ exports.updateApp = asyncWrapper(async (req, res, next) => { }); if (!app) { - return next(new ErrorResponse(`App with id of ${req.params.id} was not found`, 404)); + return next( + new ErrorResponse(`App with id of ${req.params.id} was not found`, 404) + ); } let _body = { ...req.body }; @@ -166,8 +178,8 @@ exports.updateApp = asyncWrapper(async (req, res, next) => { res.status(200).json({ success: true, data: app - }) -}) + }); +}); // @desc Delete app // @route DELETE /api/apps/:id @@ -175,26 +187,29 @@ exports.updateApp = asyncWrapper(async (req, res, next) => { 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 } - }) - }) + await App.update( + { orderId }, + { + where: { id } + } + ); + }); res.status(200).json({ success: true, data: {} - }) -}) \ No newline at end of file + }); +}); diff --git a/utils/initialConfig.json b/utils/initialConfig.json index 0d0613c..99bbb46 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -66,11 +66,11 @@ }, { "key": "dockerApps", - "value": true + "value": false }, { "key": "unpinStoppedApps", - "value": true + "value": false } ] } From 1699146f799b27da4b40798d407c1e63c37807ee Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 6 Aug 2021 15:15:54 +0200 Subject: [PATCH 009/166] Added support for custom SVG icons --- CHANGELOG.md | 3 + client/.env | 2 +- client/package-lock.json | 13 + client/package.json | 1 + client/src/App.tsx | 5 +- .../Apps/AppCard/AppCard.module.css | 6 +- .../src/components/Apps/AppCard/AppCard.tsx | 42 ++- .../src/components/Apps/AppForm/AppForm.tsx | 128 +++---- .../BookmarkCard/BookmarkCard.module.css | 12 +- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 55 ++- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 329 +++++++++--------- controllers/apps.js | 12 +- db.js | 10 +- middleware/multer.js | 8 +- 14 files changed, 356 insertions(+), 270 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6df0e5..b1bd00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.6.3 (TBA) +- Added support for custom SVG icons ([#73](https://github.com/pawelmalak/flame/issues/73)) + ### v1.6.2 (2021-08-06) - Fixed changelog link - Added support for Docker API ([#14](https://github.com/pawelmalak/flame/issues/14)) diff --git a/client/.env b/client/.env index 0c25886..3ab31e3 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.2 \ No newline at end of file +REACT_APP_VERSION=1.6.3 \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 66b371f..8326fc6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -6462,6 +6462,14 @@ } } }, + "external-svg-loader": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/external-svg-loader/-/external-svg-loader-1.3.4.tgz", + "integrity": "sha512-73h7/rYYA4KnIV74M/0r6zHWPLuY/8QHnwKymwh+46tbQAZ0ZtoN98TJZI+CUYTfP2nXgqslCgSsxcr7eOw45w==", + "requires": { + "idb-keyval": "^3.2.0" + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -7527,6 +7535,11 @@ "postcss": "^7.0.14" } }, + "idb-keyval": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-3.2.0.tgz", + "integrity": "sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==" + }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", diff --git a/client/package.json b/client/package.json index 832d079..f50079e 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "axios": "^0.21.1", + "external-svg-loader": "^1.3.4", "http-proxy-middleware": "^2.0.0", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 157206e..05db805 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { getConfig, setTheme } from './store/actions'; +import 'external-svg-loader'; // Redux import { store } from './store/store'; @@ -40,6 +41,6 @@ const App = (): JSX.Element => { ); -} +}; -export default App; \ No newline at end of file +export default App; diff --git a/client/src/components/Apps/AppCard/AppCard.module.css b/client/src/components/Apps/AppCard/AppCard.module.css index 768ef8e..d6b13a8 100644 --- a/client/src/components/Apps/AppCard/AppCard.module.css +++ b/client/src/components/Apps/AppCard/AppCard.module.css @@ -33,11 +33,11 @@ .AppCard { padding: 2px; border-radius: 4px; - transition: all 0.10s; + transition: all 0.1s; } .AppCard:hover { - background-color: rgba(0,0,0,0.2); + background-color: rgba(0, 0, 0, 0.2); } } @@ -47,4 +47,4 @@ margin-top: 2px; margin-left: 2px; object-fit: contain; -} \ No newline at end of file +} diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 79ad3d8..172a680 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -13,6 +13,31 @@ interface ComponentProps { const AppCard = (props: ComponentProps): JSX.Element => { const [displayUrl, redirectUrl] = urlParser(props.app.url); + let iconEl: JSX.Element; + const { icon } = props.app; + + if (/.(jpeg|jpg|png)$/i.test(icon)) { + iconEl = ( + {`${props.app.name} + ); + } else if (/.(svg)$/i.test(icon)) { + iconEl = ( +
+ +
+ ); + } else { + iconEl = ; + } + return (
{ rel='noreferrer' className={classes.AppCard} > -
- {(/.(jpeg|jpg|png)$/i).test(props.app.icon) - ? {`${props.app.name} - : - } -
+
{iconEl}
{props.app.name}
{displayUrl}
- ) -} + ); +}; -export default AppCard; \ No newline at end of file +export default AppCard; diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 72d8db2..5d05f0a 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -31,28 +31,28 @@ const AppForm = (props: ComponentProps): JSX.Element => { name: props.app.name, url: props.app.url, icon: props.app.icon - }) + }); } else { setFormData({ name: '', url: '', icon: '' - }) + }); } - }, [props.app]) + }, [props.app]); const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, [e.target.name]: e.target.value - }) - } + }); + }; const fileChangeHandler = (e: ChangeEvent): void => { if (e.target.files) { setCustomIcon(e.target.files[0]); } - } + }; const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); @@ -66,7 +66,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { data.append('url', formData.url); return data; - } + }; if (!props.app) { if (customIcon) { @@ -89,10 +89,10 @@ const AppForm = (props: ComponentProps): JSX.Element => { name: '', url: '', icon: '' - }) + }); setCustomIcon(null); - } + }; return ( { placeholder='Bookstack' required value={formData.name} - onChange={(e) => inputChangeHandler(e)} + onChange={e => inputChangeHandler(e)} /> @@ -120,7 +120,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { placeholder='bookstack.example.com' required value={formData.url} - onChange={(e) => inputChangeHandler(e)} + onChange={e => inputChangeHandler(e)} /> { target='_blank' rel='noreferrer' > - {' '}Check supported URL formats + {' '} + Check supported URL formats - {!useCustomIcon + {!useCustomIcon ? ( // use mdi icon - ? ( - - inputChangeHandler(e)} - /> - - Use icon name from MDI. - - {' '}Click here for reference - - - toggleUseCustomIcon(!useCustomIcon)} - className={classes.Switch}> - Switch to custom icon upload - - ) + + + inputChangeHandler(e)} + /> + + Use icon name from MDI. + + {' '} + Click here for reference + + + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch} + > + Switch to custom icon upload + + + ) : ( // upload custom icon - : ( - - fileChangeHandler(e)} - accept='.jpg,.jpeg,.png' - /> - toggleUseCustomIcon(!useCustomIcon)} - className={classes.Switch}> - Switch to MDI - - ) - } - {!props.app - ? - : - } + + + fileChangeHandler(e)} + accept='.jpg,.jpeg,.png,.svg' + /> + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch} + > + Switch to MDI + + + )} + {!props.app ? ( + + ) : ( + + )} - ) -} + ); +}; -export default connect(null, { addApp, updateApp })(AppForm); \ No newline at end of file +export default connect(null, { addApp, updateApp })(AppForm); diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css index b21ed42..ec5cbfd 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -32,4 +32,14 @@ display: flex; margin-top: 3px; margin-right: 2px; -} \ No newline at end of file + justify-content: center; + align-items: center; +} + +.BookmarkIconSvg { + width: 80%; + height: 80%; + margin-top: 2px; + margin-left: 2px; + object-fit: contain; +} diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index fe2198b..d3c0b2d 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -16,31 +16,52 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { {props.category.bookmarks.map((bookmark: Bookmark) => { const redirectUrl = urlParser(bookmark.url)[1]; + let iconEl: JSX.Element; + const { icon, name } = bookmark; + + if (/.(jpeg|jpg|png)$/i.test(icon)) { + iconEl = ( +
+ {`${name} +
+ ); + } else if (/.(svg)$/i.test(icon)) { + iconEl = ( +
+ +
+ ); + } else { + iconEl = ( +
+ +
+ ); + } + return ( - {bookmark.icon && ( -
- {(/.(jpeg|jpg|png)$/i).test(bookmark.icon) - ? {`${bookmark.name} - : - } -
- )} + key={`bookmark-${bookmark.id}`} + > + {icon && iconEl} {bookmark.name}
- ) + ); })}
- ) -} + ); +}; -export default BookmarkCard; \ No newline at end of file +export default BookmarkCard; diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 67059ae..10d6de2 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -1,11 +1,31 @@ -import { useState, SyntheticEvent, Fragment, ChangeEvent, useEffect } from 'react'; +import { + useState, + SyntheticEvent, + Fragment, + ChangeEvent, + useEffect +} from 'react'; import { connect } from 'react-redux'; import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import { Bookmark, Category, GlobalState, NewBookmark, NewCategory, NewNotification } from '../../../interfaces'; +import { + Bookmark, + Category, + GlobalState, + NewBookmark, + NewCategory, + NewNotification +} from '../../../interfaces'; import { ContentType } from '../Bookmarks'; -import { getCategories, addCategory, addBookmark, updateCategory, updateBookmark, createNotification } from '../../../store/actions'; +import { + getCategories, + addCategory, + addBookmark, + updateCategory, + updateBookmark, + createNotification +} from '../../../store/actions'; import Button from '../../UI/Buttons/Button/Button'; import classes from './BookmarkForm.module.css'; @@ -22,8 +42,8 @@ interface ComponentProps { id: number, formData: NewBookmark | FormData, category: { - prev: number, - curr: number + prev: number; + curr: number; } ) => void; createNotification: (notification: NewNotification) => void; @@ -34,14 +54,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const [customIcon, setCustomIcon] = useState(null); const [categoryName, setCategoryName] = useState({ name: '' - }) + }); const [formData, setFormData] = useState({ name: '', url: '', categoryId: -1, icon: '' - }) + }); // Load category data if provided for editing useEffect(() => { @@ -50,7 +70,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { } else { setCategoryName({ name: '' }); } - }, [props.category]) + }, [props.category]); // Load bookmark data if provided for editing useEffect(() => { @@ -60,16 +80,16 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { url: props.bookmark.url, categoryId: props.bookmark.categoryId, icon: props.bookmark.icon - }) + }); } else { setFormData({ name: '', url: '', categoryId: -1, icon: '' - }) + }); } - }, [props.bookmark]) + }, [props.bookmark]); const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); @@ -84,7 +104,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { data.append('categoryId', `${formData.categoryId}`); return data; - } + }; if (!props.category && !props.bookmark) { // Add new @@ -98,7 +118,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { props.createNotification({ title: 'Error', message: 'Please select category' - }) + }); return; } @@ -108,15 +128,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { } else { props.addBookmark(formData); } - + setFormData({ name: '', url: '', categoryId: formData.categoryId, icon: '' - }) + }); - setCustomIcon(null) + setCustomIcon(null); } } else { // Update @@ -128,23 +148,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { // Update bookmark if (customIcon) { const data = createFormData(); - props.updateBookmark( - props.bookmark.id, - data, - { - prev: props.bookmark.categoryId, - curr: formData.categoryId - } - ) + props.updateBookmark(props.bookmark.id, data, { + prev: props.bookmark.categoryId, + curr: formData.categoryId + }); } else { - props.updateBookmark( - props.bookmark.id, - formData, - { - prev: props.bookmark.categoryId, - curr: formData.categoryId - } - ); + props.updateBookmark(props.bookmark.id, formData, { + prev: props.bookmark.categoryId, + curr: formData.categoryId + }); } setFormData({ @@ -152,36 +164,36 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { url: '', categoryId: -1, icon: '' - }) + }); - setCustomIcon(null) + setCustomIcon(null); } props.modalHandler(); } - } + }; const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, [e.target.name]: e.target.value - }) - } + }); + }; const selectChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, categoryId: parseInt(e.target.value) - }) - } + }); + }; const fileChangeHandler = (e: ChangeEvent): void => { if (e.target.files) { setCustomIcon(e.target.files[0]); } - } + }; - let button = + let button = ; if (!props.category && !props.bookmark) { if (props.contentType === ContentType.category) { @@ -190,9 +202,9 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { button = ; } } else if (props.category) { - button = + button = ; } else if (props.bookmark) { - button = + button = ; } return ( @@ -200,136 +212,133 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { modalHandler={props.modalHandler} formHandler={formSubmitHandler} > - {props.contentType === ContentType.category - ? ( - + {props.contentType === ContentType.category ? ( + + + + setCategoryName({ name: e.target.value })} + /> + + + ) : ( + + + + inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + + {' '} + Check supported URL formats + + + + + + + + {!useCustomIcon ? ( + // mdi - + setCategoryName({ name: e.target.value })} - /> - - - ) - : ( - - - - inputChangeHandler(e)} - /> - - - - inputChangeHandler(e)} + name='icon' + id='icon' + placeholder='book-open-outline' + value={formData.icon} + onChange={e => inputChangeHandler(e)} /> - - {' '}Check supported URL formats + Use icon name from MDI. + + {' '} + Click here for reference - - - - + Switch to custom icon upload + - {!useCustomIcon - // mdi - ? ( - - inputChangeHandler(e)} - /> - - Use icon name from MDI. - - {' '}Click here for reference - - - toggleUseCustomIcon(!useCustomIcon)} - className={classes.Switch}> - Switch to custom icon upload - - ) - // custom - : ( - - fileChangeHandler(e)} - accept='.jpg,.jpeg,.png' - /> - toggleUseCustomIcon(!useCustomIcon)} - className={classes.Switch}> - Switch to MDI - - ) - } - - ) - } + ) : ( + // custom + + + fileChangeHandler(e)} + accept='.jpg,.jpeg,.png,.svg' + /> + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch} + > + Switch to MDI + + + )} + + )} {button} - ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { categories: state.bookmark.categories - } -} + }; +}; const dispatchMap = { getCategories, @@ -338,6 +347,6 @@ const dispatchMap = { updateCategory, updateBookmark, createNotification -} +}; -export default connect(mapStateToProps, dispatchMap)(BookmarkForm); \ No newline at end of file +export default connect(mapStateToProps, dispatchMap)(BookmarkForm); diff --git a/controllers/apps.js b/controllers/apps.js index e4fa1bc..ab59f2c 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -126,8 +126,16 @@ exports.getApps = asyncWrapper(async (req, res, next) => { }); } - // Set header to fetch containers info every time - res.status(200).setHeader('Cache-Control', 'no-store').json({ + 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 }); diff --git a/db.js b/db.js index 9761efe..f9cbcfd 100644 --- a/db.js +++ b/db.js @@ -6,15 +6,15 @@ const sequelize = new Sequelize({ dialect: 'sqlite', storage: './data/db.sqlite', logging: false -}) +}); const connectDB = async () => { try { await sequelize.authenticate(); logger.log('Connected to database'); - + const syncModels = true; - + if (syncModels) { logger.log('Starting model synchronization'); await sequelize.sync({ alter: true }); @@ -24,9 +24,9 @@ const connectDB = async () => { logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR'); process.exit(1); } -} +}; module.exports = { connectDB, sequelize -} \ No newline at end of file +}; diff --git a/middleware/multer.js b/middleware/multer.js index b1314a9..bd493f5 100644 --- a/middleware/multer.js +++ b/middleware/multer.js @@ -12,9 +12,9 @@ const storage = multer.diskStorage({ filename: (req, file, cb) => { cb(null, Date.now() + '--' + file.originalname); } -}) +}); -const supportedTypes = ['jpg', 'jpeg', 'png']; +const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml']; const fileFilter = (req, file, cb) => { if (supportedTypes.includes(file.mimetype.split('/')[1])) { @@ -22,8 +22,8 @@ const fileFilter = (req, file, cb) => { } else { cb(null, false); } -} +}; const upload = multer({ storage, fileFilter }); -module.exports = upload.single('icon'); \ No newline at end of file +module.exports = upload.single('icon'); From 683c948f6c433f07dbade0b496e1c8d03b9ce3f6 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 6 Aug 2021 16:16:13 +0200 Subject: [PATCH 010/166] Added cli tool for adding new search engines/providers --- .prettierrc | 8 ++++ api.js | 5 +-- client/package-lock.json | 6 +++ client/package.json | 3 ++ client/src/utility/searchQueries.json | 40 +++++++++---------- client/utils/dev/cli-searchQueries.js | 57 +++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 .prettierrc create mode 100644 client/utils/dev/cli-searchQueries.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f14788c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "arrowParens": "always", + "printWidth": 80, + "trailingComma": "es5" +} diff --git a/api.js b/api.js index a720fe2..1c2d863 100644 --- a/api.js +++ b/api.js @@ -9,8 +9,7 @@ api.use(express.static(join(__dirname, 'public'))); api.use('/uploads', express.static(join(__dirname, 'data/uploads'))); api.get(/^\/(?!api)/, (req, res) => { res.sendFile(join(__dirname, 'public/index.html')); -}) - +}); // Body parser api.use(express.json()); @@ -25,4 +24,4 @@ api.use('/api/bookmarks', require('./routes/bookmark')); // Custom error handler api.use(errorHandler); -module.exports = api; \ No newline at end of file +module.exports = api; diff --git a/client/package-lock.json b/client/package-lock.json index 8326fc6..2717839 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12113,6 +12113,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/client/package.json b/client/package.json index f50079e..6e05667 100644 --- a/client/package.json +++ b/client/package.json @@ -54,5 +54,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "prettier": "^2.3.2" } } diff --git a/client/src/utility/searchQueries.json b/client/src/utility/searchQueries.json index 6e23503..c4e055d 100644 --- a/client/src/utility/searchQueries.json +++ b/client/src/utility/searchQueries.json @@ -1,9 +1,9 @@ { "queries": [ { - "name": "Google", - "prefix": "g", - "template": "https://www.google.com/search?q=" + "name": "Disroot", + "prefix": "ds", + "template": "http://search.disroot.org/search?q=" }, { "name": "DuckDuckGo", @@ -11,19 +11,9 @@ "template": "https://duckduckgo.com/?q=" }, { - "name": "Disroot", - "prefix": "ds", - "template": "http://search.disroot.org/search?q=" - }, - { - "name": "YouTube", - "prefix": "yt", - "template": "https://www.youtube.com/results?search_query=" - }, - { - "name": "Reddit", - "prefix": "r", - "template": "https://www.reddit.com/search?q=" + "name": "Google", + "prefix": "g", + "template": "https://www.google.com/search?q=" }, { "name": "IMDb", @@ -31,14 +21,24 @@ "template": "https://www.imdb.com/find?q=" }, { - "name": "The Movie Database", - "prefix": "mv", - "template": "https://www.themoviedb.org/search?query=" + "name": "Reddit", + "prefix": "r", + "template": "https://www.reddit.com/search?q=" }, { "name": "Spotify", "prefix": "sp", "template": "https://open.spotify.com/search/" + }, + { + "name": "The Movie Database", + "prefix": "mv", + "template": "https://www.themoviedb.org/search?query=" + }, + { + "name": "YouTube", + "prefix": "yt", + "template": "https://www.youtube.com/results?search_query=" } ] -} \ No newline at end of file +} diff --git a/client/utils/dev/cli-searchQueries.js b/client/utils/dev/cli-searchQueries.js new file mode 100644 index 0000000..c431b32 --- /dev/null +++ b/client/utils/dev/cli-searchQueries.js @@ -0,0 +1,57 @@ +const queries = require('../../src/utility/searchQueries.json'); +const fs = require('fs'); +const prettier = require('prettier'); + +/** + * @description CLI tool for adding new search engines/providers. It will ensure that prefix is unique and that all entries are sorted alphabetically + * @argumens name prefix template + * @example node cli-searchQueries.js "DuckDuckGo" "d" "https://duckduckgo.com/?q=" + */ + +// Get arguments +const args = process.argv.slice(2); + +// Check arguments +if (args.length < 3) { + return console.log('Missing arguments'); +} else if (args.length > 3) { + return console.log('Too many arguments provided'); +} + +// Construct new query object +const newQuery = { + name: args[0], + prefix: args[1], + template: args[2], +}; + +// Get old queries +let rawQueries = queries.queries; +let parsedQueries = ''; + +// Check if prefix is unique +const isUnique = !rawQueries.find((query) => query.prefix == newQuery.prefix); + +if (!isUnique) { + return console.log('Prefix already exists'); +} + +// Add new query +rawQueries.push(newQuery); + +// Sort alphabetically +rawQueries = rawQueries.sort((a, b) => { + const _a = a.name.toLowerCase(); + const _b = b.name.toLowerCase(); + + if (_a < _b) return -1; + if (_a > _b) return 1; + return 0; +}); + +// Format JSON +parsedQueries = JSON.stringify(queries); +parsedQueries = prettier.format(parsedQueries, { parser: 'json' }); + +// Save file +fs.writeFileSync('../../src/utility/searchQueries.json', parsedQueries); From 5c60c7c1568a77401b7783ba92c8f3ced8f34d9b Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Aug 2021 12:54:07 +0200 Subject: [PATCH 011/166] Pushed version 1.6.3. Added Deezer and Tidal to search queries --- CHANGELOG.md | 3 ++- README.md | 10 ++-------- client/src/utility/searchQueries.json | 10 ++++++++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1bd00e..3101629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -### v1.6.3 (TBA) +### v1.6.3 (2021-08-09) - Added support for custom SVG icons ([#73](https://github.com/pawelmalak/flame/issues/73)) +- Added Deezer and Tidal to search queries ### v1.6.2 (2021-08-06) - Fixed changelog link diff --git a/README.md b/README.md index c15bf65..ef01382 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ services: container_name: flame volumes: - :/app/data - - /var/run/docker.sock:/var/sock/docker.sock # optional but required for Docker integration feature + - /var/run/docker.sock:/var/run/docker.sock # optional but required for Docker integration feature ports: - 5005:5005 restart: unless-stopped @@ -174,10 +174,4 @@ And you must have activated the Docker sync option in the settings panel. > This is an experimental feature. Its behaviour might change in the future. > -> Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) - -## Support - -If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link: - -[![PayPal Badge](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/pawelmalak) +> Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) \ No newline at end of file diff --git a/client/src/utility/searchQueries.json b/client/src/utility/searchQueries.json index c4e055d..e154245 100644 --- a/client/src/utility/searchQueries.json +++ b/client/src/utility/searchQueries.json @@ -1,5 +1,10 @@ { "queries": [ + { + "name": "Deezer", + "prefix": "dz", + "template": "https://www.deezer.com/search/" + }, { "name": "Disroot", "prefix": "ds", @@ -35,6 +40,11 @@ "prefix": "mv", "template": "https://www.themoviedb.org/search?query=" }, + { + "name": "Tidal", + "prefix": "td", + "template": "https://listen.tidal.com/search?q=" + }, { "name": "YouTube", "prefix": "yt", From 78a018f686be4d384622489315ae7ce65980ac02 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 9 Aug 2021 15:31:20 +0200 Subject: [PATCH 012/166] Bookmark icon fixes --- .../BookmarkCard/BookmarkCard.module.css | 7 ++ .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 64 ++++++++++--------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css index ec5cbfd..b840a42 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -43,3 +43,10 @@ margin-left: 2px; object-fit: contain; } + +.CustomIcon { + width: 90%; + height: 90%; + margin-top: 2px; + object-fit: contain; +} diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index d3c0b2d..b332a6f 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -3,6 +3,7 @@ import classes from './BookmarkCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; import { iconParser, urlParser, searchConfig } from '../../../utility'; +import { Fragment } from 'react'; interface ComponentProps { category: Category; @@ -16,45 +17,48 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { {props.category.bookmarks.map((bookmark: Bookmark) => { const redirectUrl = urlParser(bookmark.url)[1]; - let iconEl: JSX.Element; - const { icon, name } = bookmark; + let iconEl: JSX.Element = ; - if (/.(jpeg|jpg|png)$/i.test(icon)) { - iconEl = ( -
- {`${name} -
- ); - } else if (/.(svg)$/i.test(icon)) { - iconEl = ( -
- -
- ); - } else { - iconEl = ( -
- -
- ); + if (bookmark.icon) { + const { icon, name } = bookmark; + + if (/.(jpeg|jpg|png)$/i.test(icon)) { + iconEl = ( +
+ {`${name} +
+ ); + } else if (/.(svg)$/i.test(icon)) { + iconEl = ( +
+ +
+ ); + } else { + iconEl = ( +
+ +
+ ); + } } return ( - {icon && iconEl} + {bookmark.icon && iconEl} {bookmark.name} ); From 8681f75babac390b7d7b7cadadaf0bb359e4e72a Mon Sep 17 00:00:00 2001 From: Dimitri Pommier Date: Tue, 17 Aug 2021 10:32:15 +0200 Subject: [PATCH 013/166] Kubernetes integration (#80) * chore(): skaffold * chore(): kubernetes integration * chore(skaffold): refine shokohsc profile * chore(): removed docker & kubernetes from database + stoppedApp pin option * Revert "chore(): removed docker & kubernetes from database + stoppedApp pin option" This reverts commit 5111c7ad794d15e157b04edcb270800d08257cec. --- .dockerignore | 4 +- CHANGELOG.md | 2 +- Dockerfile.dev | 16 + README.md | 23 + client/.env | 2 +- .../Settings/OtherSettings/OtherSettings.tsx | 17 + client/src/interfaces/Forms.ts | 1 + controllers/apps.js | 62 + k8s/base/deployment.yaml | 28 + k8s/base/ingress.yaml | 17 + k8s/base/kustomization.yaml | 9 + k8s/base/namespace.yaml | 8 + k8s/base/rbac.yaml | 26 + k8s/base/service.yaml | 16 + k8s/overlays/shokohsc/deployment.yaml | 36 + k8s/overlays/shokohsc/ingress.yaml | 28 + k8s/overlays/shokohsc/kustomization.yaml | 9 + k8s/overlays/shokohsc/namespace.yaml | 8 + k8s/overlays/shokohsc/rbac.yaml | 26 + k8s/overlays/shokohsc/service.yaml | 16 + package-lock.json | 5229 ++++++++++++++++- package.json | 4 +- skaffold.yaml | 65 + utils/initialConfig.json | 4 + 24 files changed, 5567 insertions(+), 89 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 k8s/base/deployment.yaml create mode 100644 k8s/base/ingress.yaml create mode 100644 k8s/base/kustomization.yaml create mode 100644 k8s/base/namespace.yaml create mode 100644 k8s/base/rbac.yaml create mode 100644 k8s/base/service.yaml create mode 100644 k8s/overlays/shokohsc/deployment.yaml create mode 100644 k8s/overlays/shokohsc/ingress.yaml create mode 100644 k8s/overlays/shokohsc/kustomization.yaml create mode 100644 k8s/overlays/shokohsc/namespace.yaml create mode 100644 k8s/overlays/shokohsc/rbac.yaml create mode 100644 k8s/overlays/shokohsc/service.yaml create mode 100644 skaffold.yaml diff --git a/.dockerignore b/.dockerignore index da9bc10..6c10c72 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ node_modules github public -build.sh \ No newline at end of file +build.sh +k8s +skaffold.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3101629..e269e15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,4 +56,4 @@ - Added 'warnings' to apps and bookmarks forms about supported url formats ([#5](https://github.com/pawelmalak/flame/issues/5)) ### v1.0 (2021-06-08) -Initial release of Flame - self-hosted startpage using Node.js on backend and React on frontend. \ No newline at end of file +Initial release of Flame - self-hosted startpage using Node.js on backend and React on frontend. diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..680ed26 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,16 @@ +FROM node:lts-alpine as build-front +RUN apk add --no-cache curl +WORKDIR /app +COPY ./client . +RUN npm install --production \ + && npm run build + +FROM node:lts-alpine +WORKDIR /app +RUN mkdir -p ./public +COPY --from=build-front /app/build/ ./public + +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["npm", "run", "skaffold"] diff --git a/README.md b/README.md index ef01382..dadb428 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Flame is self-hosted startpage for your server. Its design is inspired (heavily) - TypeScript - Deployment - Docker + - Kubernetes ## Development @@ -80,6 +81,13 @@ services: restart: unless-stopped ``` +#### Skaffold + +```sh +# use skaffold +skaffold dev +``` + ### Without Docker Follow instructions from wiki: [Installation without Docker](https://github.com/pawelmalak/flame/wiki/Installation-without-docker) @@ -170,6 +178,21 @@ labels: And you must have activated the Docker sync option in the settings panel. +### Kubernetes integration + +In order to use the Kubernetes integration, each ingress must have the following annotations: + +```yml +metadata: + annotations: + - flame.pawelmalak/type=application # "app" works too + - flame.pawelmalak/name=My container + - flame.pawelmalak/url=https://example.com + - flame.pawelmalak/icon=icon-name # Optional, default is "kubernetes" +``` + +And you must have activated the Kubernetes sync option in the settings panel. + ### Custom CSS > This is an experimental feature. Its behaviour might change in the future. diff --git a/client/.env b/client/.env index 3ab31e3..3395c3f 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.3 \ No newline at end of file +REACT_APP_VERSION=1.6.3 diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 80b0c0b..31bbd52 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -52,6 +52,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { bookmarksSameTab: 0, searchSameTab: 0, dockerApps: 1, + kubernetesApps: 1, unpinStoppedApps: 1 }); @@ -71,6 +72,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { bookmarksSameTab: searchConfig('bookmarksSameTab', 0), searchSameTab: searchConfig('searchSameTab', 0), dockerApps: searchConfig('dockerApps', 0), + kubernetesApps: searchConfig('kubernetesApps', 0), unpinStoppedApps: searchConfig('unpinStoppedApps', 0) }); }, [props.loading]); @@ -297,6 +299,21 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { + + {/* KUBERNETES SETTINGS */} +

Kubernetes

+ + + + ); diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 8717d03..177821d 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -19,5 +19,6 @@ export interface SettingsForm { bookmarksSameTab: number; searchSameTab: number; dockerApps: number; + kubernetesApps: number; unpinStoppedApps: number; } diff --git a/controllers/apps.js b/controllers/apps.js index ab59f2c..b5c66be 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -6,6 +6,7 @@ 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 @@ -51,6 +52,9 @@ exports.getApps = asyncWrapper(async (req, res, next) => { const useDockerApi = await Config.findOne({ where: { key: 'dockerApps' } }); + const useKubernetesApi = await Config.findOne({ + where: { key: 'kubernetesApps' } + }); const unpinStoppedApps = await Config.findOne({ where: { key: 'unpinStoppedApps' } }); @@ -116,6 +120,64 @@ exports.getApps = asyncWrapper(async (req, res, next) => { } } + 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']] diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml new file mode 100644 index 0000000..3a33cee --- /dev/null +++ b/k8s/base/deployment.yaml @@ -0,0 +1,28 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: flame +spec: + selector: + matchLabels: + app: flame + template: + metadata: + labels: + app: flame + spec: + serviceAccountName: flame + securityContext: + fsGroup: 1000 + containers: + - name: flame + image: shokohsc/flame + ports: + - name: http + containerPort: 5005 + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml new file mode 100644 index 0000000..7694ddb --- /dev/null +++ b/k8s/base/ingress.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flame +spec: + rules: + - host: flame.cluster.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: flame + port: + number: 80 diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..5fbfb6f --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: flame +resources: + - namespace.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + - rbac.yaml diff --git a/k8s/base/namespace.yaml b/k8s/base/namespace.yaml new file mode 100644 index 0000000..cf4094e --- /dev/null +++ b/k8s/base/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flame + labels: + namespace: flame + goldilocks.fairwinds.com/enabled: "true" diff --git a/k8s/base/rbac.yaml b/k8s/base/rbac.yaml new file mode 100644 index 0000000..eedb941 --- /dev/null +++ b/k8s/base/rbac.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flame +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: flame +rules: +- apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flame +subjects: +- kind: ServiceAccount + name: flame +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flame diff --git a/k8s/base/service.yaml b/k8s/base/service.yaml new file mode 100644 index 0000000..d6cf340 --- /dev/null +++ b/k8s/base/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: flame + labels: + app: flame +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: flame diff --git a/k8s/overlays/shokohsc/deployment.yaml b/k8s/overlays/shokohsc/deployment.yaml new file mode 100644 index 0000000..8649fac --- /dev/null +++ b/k8s/overlays/shokohsc/deployment.yaml @@ -0,0 +1,36 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: flame +spec: + selector: + matchLabels: + app: flame + template: + metadata: + labels: + app: flame + spec: + serviceAccountName: flame-dev + securityContext: + fsGroup: 1000 + containers: + - name: flame + image: shokohsc/flame + command: + - npm + args: + - run + - skaffold + env: + - name: NODE_ENV + value: development + ports: + - name: http + containerPort: 5005 + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http diff --git a/k8s/overlays/shokohsc/ingress.yaml b/k8s/overlays/shokohsc/ingress.yaml new file mode 100644 index 0000000..5d523fa --- /dev/null +++ b/k8s/overlays/shokohsc/ingress.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: flame + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: ca-cluster-issuer + flame.pawelmalak/name: flame + flame.pawelmalak/url: dev.flame.shokohsc.home + flame.pawelmalak/type: app + flame.pawelmalak/icon: fire +spec: + rules: + - host: dev.flame.shokohsc.home + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: flame + port: + number: 80 + tls: + - hosts: + - dev.flame.shokohsc.home + secretName: flame-cert diff --git a/k8s/overlays/shokohsc/kustomization.yaml b/k8s/overlays/shokohsc/kustomization.yaml new file mode 100644 index 0000000..8eff20c --- /dev/null +++ b/k8s/overlays/shokohsc/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: flame-dev +resources: + - namespace.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + - rbac.yaml diff --git a/k8s/overlays/shokohsc/namespace.yaml b/k8s/overlays/shokohsc/namespace.yaml new file mode 100644 index 0000000..d767629 --- /dev/null +++ b/k8s/overlays/shokohsc/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flame-dev + labels: + namespace: flame-dev + goldilocks.fairwinds.com/enabled: "true" diff --git a/k8s/overlays/shokohsc/rbac.yaml b/k8s/overlays/shokohsc/rbac.yaml new file mode 100644 index 0000000..2fdc1e6 --- /dev/null +++ b/k8s/overlays/shokohsc/rbac.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: flame-dev +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: flame-dev +rules: +- apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flame-dev +subjects: +- kind: ServiceAccount + name: flame-dev +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flame-dev diff --git a/k8s/overlays/shokohsc/service.yaml b/k8s/overlays/shokohsc/service.yaml new file mode 100644 index 0000000..d6cf340 --- /dev/null +++ b/k8s/overlays/shokohsc/service.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: flame + labels: + app: flame +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: flame diff --git a/package-lock.json b/package-lock.json index 21017a1..5abdad6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,4517 @@ { "name": "flame", "version": "0.1.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "flame", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@kubernetes/client-node": "^0.15.0", + "@types/express": "^4.17.11", + "axios": "^0.21.1", + "colors": "^1.4.0", + "concurrently": "^6.0.2", + "dotenv": "^9.0.0", + "express": "^4.17.1", + "multer": "^1.4.2", + "node-schedule": "^2.0.0", + "sequelize": "^6.6.2", + "sqlite3": "^5.0.2", + "ws": "^7.4.6" + }, + "devDependencies": { + "nodemon": "^2.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" + }, + "node_modules/@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/@kubernetes/client-node": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.15.0.tgz", + "integrity": "sha512-AnEcsWWadl5IWOzzvO/gWpTnJb1d1CzA/rbV/qK1c0fD1SOxTDPj6jFllyQ9icGDfCgNw3TafZftmuepm6z9JA==", + "dependencies": { + "@types/js-yaml": "^3.12.1", + "@types/node": "^10.12.0", + "@types/request": "^2.47.1", + "@types/stream-buffers": "^3.0.3", + "@types/tar": "^4.0.3", + "@types/underscore": "^1.8.9", + "@types/ws": "^6.0.1", + "byline": "^5.0.0", + "execa": "5.0.0", + "isomorphic-ws": "^4.0.1", + "js-yaml": "^3.13.1", + "jsonpath-plus": "^0.19.0", + "openid-client": "^4.1.1", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "shelljs": "^0.8.4", + "stream-buffers": "^3.0.2", + "tar": "^6.0.2", + "tmp-promise": "^3.0.2", + "tslib": "^1.9.3", + "underscore": "^1.9.1", + "ws": "^7.3.1" + } + }, + "node_modules/@kubernetes/client-node/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/@kubernetes/client-node/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@kubernetes/client-node/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@kubernetes/client-node/node_modules/minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@kubernetes/client-node/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@kubernetes/client-node/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@kubernetes/client-node/node_modules/tar": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz", + "integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "node_modules/@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", + "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "node_modules/@types/js-yaml": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.7.tgz", + "integrity": "sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ==" + }, + "node_modules/@types/keyv": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz", + "integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minipass": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.0.tgz", + "integrity": "sha512-b2yPKwCrB8x9SB65kcCistMoe3wrYnxxt5rJSZ1kprw0uOXvhuKi9kTQ746Y+Pbqoh+9C0N4zt0ztmTnG9yg7A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" + }, + "node_modules/@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "node_modules/@types/request": { + "version": "2.48.7", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.7.tgz", + "integrity": "sha512-GWP9AZW7foLd4YQxyFZDBepl0lPsWLMEXDZUjQ/c1gqVPDPECrRZyEzuhJdnPWioFCq3Tv0qoGpMD6U+ygd4ZA==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stream-buffers": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.4.tgz", + "integrity": "sha512-qU/K1tb2yUdhXkLIATzsIPwbtX6BpZk0l3dPW6xqWyhfzzM1ECaQ/8faEnu3CNraLiQ9LHyQQPBGp7N9Fbs25w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tar": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.5.tgz", + "integrity": "sha512-cgwPhNEabHaZcYIy5xeMtux2EmYBitfqEceBUi2t5+ETy4dW6kswt6WX4+HqLeiiKOo42EXbGiDmVJ2x+vi37Q==", + "dependencies": { + "@types/minipass": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", + "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + }, + "node_modules/@types/underscore": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.3.tgz", + "integrity": "sha512-Fl1TX1dapfXyDqFg2ic9M+vlXRktcPJrc4PR7sRc7sdVrjavg/JHlbUXBt8qWWqhJrmSqg3RNAkAPRiOYw6Ahw==" + }, + "node_modules/@types/ws": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", + "integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "dependencies": { + "string-width": "^3.0.0" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "node_modules/are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "optional": true, + "dependencies": { + "inherits": "~2.0.0" + }, + "engines": { + "node": "0.4 || >=0.5.8" + } + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/busboy/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/busboy/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/busboy/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concurrently": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.2.tgz", + "integrity": "sha512-u+1Q0dJG5BidgUTpz9CU16yoHTt/oApFDQ3mbvHwSDgMjU7aGqy0q8ZQyaZyaNxdwRKTD872Ux3Twc6//sWA+Q==", + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "read-pkg": "^5.2.0", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cron-parser": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", + "dependencies": { + "is-nan": "^1.3.2", + "luxon": "^1.26.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-fns": { + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.2.tgz", + "integrity": "sha512-FMkG7pIPx64mGIpS2LOb3Wp3O606H/hatoiz7G0oiYWai1izdM4tF1dd7QABv2NogkIDI4wxsfLLFQSuVvDHgA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dicer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/dicer/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/dicer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.0.tgz", + "integrity": "sha512-yy3x9XjojW8ROTBePD25AcMoHqGHsvHmtfw8QWlpEXyMMXXPj6brUA464AptUvHuTPRmNz6Sd3ZLNLeJl6dHJA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/dottie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", + "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dependencies": { + "minipass": "^2.6.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dev": true, + "dependencies": { + "ini": "1.3.7" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "devOptional": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "node_modules/ignore-walk": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", + "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ini": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.3.0.tgz", + "integrity": "sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, + "dependencies": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/jsonpath-plus": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", + "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.27.0.tgz", + "integrity": "sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA==", + "engines": { + "node": "*" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "dependencies": { + "mime-db": "1.47.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dependencies": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dependencies": { + "minipass": "^2.9.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", + "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", + "dependencies": { + "moment": ">= 2.9.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/needle": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", + "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" + }, + "node_modules/node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "optional": true, + "dependencies": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "deprecated": "Please upgrade to @mapbox/node-pre-gyp: the non-scoped node-pre-gyp package is deprecated and only the @mapbox scoped package will recieve updates in the future", + "dependencies": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/node-pre-gyp/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/node-pre-gyp/node_modules/tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dependencies": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/node-pre-gyp/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/node-schedule": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.0.0.tgz", + "integrity": "sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ==", + "dependencies": { + "cron-parser": "^3.1.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/nodemon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", + "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, + "node_modules/npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dependencies": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", + "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.7.4.tgz", + "integrity": "sha512-n+RURXYuR0bBZo9i0pn+CXZSyg5JYQ1nbwEwPQvLE7EcJt/vMZ2iIMjLehl5DvCN53XUoPVZs9KAE5r6d9fxsw==", + "dependencies": { + "aggregate-error": "^3.1.0", + "got": "^11.8.0", + "jose": "^2.0.5", + "lru-cache": "^6.0.0", + "make-error": "^1.3.6", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "engines": { + "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/@sindresorhus/is": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", + "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/openid-client/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openid-client/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client/node_modules/got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/openid-client/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/openid-client/node_modules/keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/openid-client/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/openid-client/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/openid-client/node_modules/responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz", + "integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA==" + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/retry-as-promised": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", + "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", + "dependencies": { + "any-promise": "^1.3.0" + } + }, + "node_modules/rfc4648": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.0.tgz", + "integrity": "sha512-FA6W9lDNeX8WbMY31io1xWg+TpZCbeDKsBo0ocwACZiWnh9TUAyk9CCuBQuOPmYnwwdEQZmraQ2ZK7yJsxErBg==" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/sequelize": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.6.2.tgz", + "integrity": "sha512-H/zrzmTK+tis9PJaSigkuXI57nKBvNCtPQol0yxCvau1iWLzSOuq8t3tMOVeQ+Ep8QH2HoD9/+FCCIAqzUr/BQ==", + "dependencies": { + "debug": "^4.1.1", + "dottie": "^2.0.0", + "inflection": "1.12.0", + "lodash": "^4.17.20", + "moment": "^2.26.0", + "moment-timezone": "^0.5.31", + "retry-as-promised": "^3.2.0", + "semver": "^7.3.2", + "sequelize-pool": "^6.0.0", + "toposort-class": "^1.0.1", + "uuid": "^8.1.0", + "validator": "^10.11.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-6.1.0.tgz", + "integrity": "sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sqlite3": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", + "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^3.0.0", + "node-pre-gyp": "^0.11.0" + }, + "optionalDependencies": { + "node-gyp": "3.x" + }, + "peerDependencies": { + "node-gyp": "3.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "optional": true, + "dependencies": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz", + "integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "dev": true, + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/underscore": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dev": true, + "dependencies": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "engines": { + "node": ">=10" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.12.13", @@ -60,6 +4569,95 @@ } } }, + "@kubernetes/client-node": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.15.0.tgz", + "integrity": "sha512-AnEcsWWadl5IWOzzvO/gWpTnJb1d1CzA/rbV/qK1c0fD1SOxTDPj6jFllyQ9icGDfCgNw3TafZftmuepm6z9JA==", + "requires": { + "@types/js-yaml": "^3.12.1", + "@types/node": "^10.12.0", + "@types/request": "^2.47.1", + "@types/stream-buffers": "^3.0.3", + "@types/tar": "^4.0.3", + "@types/underscore": "^1.8.9", + "@types/ws": "^6.0.1", + "byline": "^5.0.0", + "execa": "5.0.0", + "isomorphic-ws": "^4.0.1", + "js-yaml": "^3.13.1", + "jsonpath-plus": "^0.19.0", + "openid-client": "^4.1.1", + "request": "^2.88.0", + "rfc4648": "^1.3.0", + "shelljs": "^0.8.4", + "stream-buffers": "^3.0.2", + "tar": "^6.0.2", + "tmp-promise": "^3.0.2", + "tslib": "^1.9.3", + "underscore": "^1.9.1", + "ws": "^7.3.1" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "tar": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz", + "integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + } + } + }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -84,6 +4682,22 @@ "@types/node": "*" } }, + "@types/cacheable-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", + "integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, "@types/connect": { "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", @@ -113,11 +4727,37 @@ "@types/range-parser": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + }, + "@types/js-yaml": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.7.tgz", + "integrity": "sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ==" + }, + "@types/keyv": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.2.tgz", + "integrity": "sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==", + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "@types/minipass": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.0.tgz", + "integrity": "sha512-b2yPKwCrB8x9SB65kcCistMoe3wrYnxxt5rJSZ1kprw0uOXvhuKi9kTQ746Y+Pbqoh+9C0N4zt0ztmTnG9yg7A==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "15.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", @@ -138,6 +4778,37 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, + "@types/request": { + "version": "2.48.7", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.7.tgz", + "integrity": "sha512-GWP9AZW7foLd4YQxyFZDBepl0lPsWLMEXDZUjQ/c1gqVPDPECrRZyEzuhJdnPWioFCq3Tv0qoGpMD6U+ygd4ZA==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/serve-static": { "version": "1.13.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", @@ -147,6 +4818,41 @@ "@types/node": "*" } }, + "@types/stream-buffers": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.4.tgz", + "integrity": "sha512-qU/K1tb2yUdhXkLIATzsIPwbtX6BpZk0l3dPW6xqWyhfzzM1ECaQ/8faEnu3CNraLiQ9LHyQQPBGp7N9Fbs25w==", + "requires": { + "@types/node": "*" + } + }, + "@types/tar": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.5.tgz", + "integrity": "sha512-cgwPhNEabHaZcYIy5xeMtux2EmYBitfqEceBUi2t5+ETy4dW6kswt6WX4+HqLeiiKOo42EXbGiDmVJ2x+vi37Q==", + "requires": { + "@types/minipass": "*", + "@types/node": "*" + } + }, + "@types/tough-cookie": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", + "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + }, + "@types/underscore": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.3.tgz", + "integrity": "sha512-Fl1TX1dapfXyDqFg2ic9M+vlXRktcPJrc4PR7sRc7sdVrjavg/JHlbUXBt8qWWqhJrmSqg3RNAkAPRiOYw6Ahw==" + }, + "@types/ws": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", + "integrity": "sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==", + "requires": { + "@types/node": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -161,11 +4867,19 @@ "negotiator": "0.6.2" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "optional": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -243,6 +4957,14 @@ "readable-stream": "^2.0.6" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -252,7 +4974,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "optional": true, "requires": { "safer-buffer": "~2.1.0" } @@ -260,26 +4981,22 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "optional": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "optional": true + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { "version": "0.21.1", @@ -298,7 +5015,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, "requires": { "tweetnacl": "^0.14.3" } @@ -406,11 +5122,21 @@ } } }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -461,8 +5187,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { "version": "3.0.0", @@ -518,6 +5243,11 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -553,7 +5283,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, "requires": { "mimic-response": "^1.0.0" } @@ -585,7 +5314,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -712,6 +5440,26 @@ "luxon": "^1.26.0" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -722,7 +5470,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "optional": true, "requires": { "assert-plus": "^1.0.0" } @@ -771,8 +5518,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "optional": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -855,7 +5601,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -881,7 +5626,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -915,11 +5659,39 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "execa": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", + "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + } + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -960,26 +5732,22 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "optional": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "optional": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fill-range": { "version": "7.0.1", @@ -1012,14 +5780,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "optional": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -1149,7 +5915,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "optional": true, "requires": { "assert-plus": "^1.0.0" } @@ -1207,19 +5972,18 @@ "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "devOptional": true }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "optional": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "optional": true, "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -1262,8 +6026,7 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-errors": { "version": "1.7.2", @@ -1281,13 +6044,26 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "optional": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1322,6 +6098,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, "inflection": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", @@ -1346,6 +6127,11 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==" }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1445,6 +6231,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -1464,25 +6255,45 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "optional": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "requires": {} }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-buffer": { "version": "3.0.0", @@ -1498,26 +6309,27 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "optional": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonpath-plus": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", + "integrity": "sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==" }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -1594,6 +6406,11 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1604,6 +6421,11 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1627,11 +6449,15 @@ "mime-db": "1.47.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, "minimatch": { "version": "3.0.4", @@ -1930,6 +6756,14 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -1949,19 +6783,28 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "optional": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "oidc-token-hash": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz", + "integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1978,6 +6821,137 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "openid-client": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.7.4.tgz", + "integrity": "sha512-n+RURXYuR0bBZo9i0pn+CXZSyg5JYQ1nbwEwPQvLE7EcJt/vMZ2iIMjLehl5DvCN53XUoPVZs9KAE5r6d9fxsw==", + "requires": { + "aggregate-error": "^3.1.0", + "got": "^11.8.0", + "jose": "^2.0.5", + "lru-cache": "^6.0.0", + "make-error": "^1.3.6", + "object-hash": "^2.0.1", + "oidc-token-hash": "^5.0.1" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", + "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==" + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.1", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -2044,6 +7018,11 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -2057,8 +7036,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "optional": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "picomatch": { "version": "2.2.3", @@ -2089,8 +7067,7 @@ "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "optional": true + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pstree.remy": { "version": "1.1.8", @@ -2102,7 +7079,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2111,8 +7087,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "optional": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pupa": { "version": "2.1.1", @@ -2128,6 +7103,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2196,6 +7176,14 @@ "picomatch": "^2.2.1" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, "registry-auth-token": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", @@ -2218,7 +7206,6 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -2245,14 +7232,12 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -2270,6 +7255,11 @@ "path-parse": "^1.0.6" } }, + "resolve-alpn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz", + "integrity": "sha512-e4FNQs+9cINYMO5NMFc6kOUCdohjqFPSgMuwuZAOUWqrfWsen+Yjy5qZFkV5K7VO7tFSLKcUL97olkED7sCBHA==" + }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -2287,6 +7277,11 @@ "any-promise": "^1.3.0" } }, + "rfc4648": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.0.tgz", + "integrity": "sha512-FA6W9lDNeX8WbMY31io1xWg+TpZCbeDKsBo0ocwACZiWnh9TUAyk9CCuBQuOPmYnwwdEQZmraQ2ZK7yJsxErBg==" + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -2436,6 +7431,29 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -2479,6 +7497,11 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "sqlite3": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz", @@ -2493,7 +7516,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "optional": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -2511,11 +7533,24 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==" + }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -2551,14 +7586,6 @@ } } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -2568,6 +7595,11 @@ "ansi-regex": "^4.1.0" } }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2598,6 +7630,32 @@ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "tmp-promise": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz", + "integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==", + "requires": { + "tmp": "^0.2.0" + } + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -2636,7 +7694,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -2656,7 +7713,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -2664,8 +7720,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-fest": { "version": "0.8.1", @@ -2705,6 +7760,11 @@ "debug": "^2.2.0" } }, + "underscore": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -2744,7 +7804,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "optional": true, "requires": { "punycode": "^2.1.0" } @@ -2796,7 +7855,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -2906,7 +7964,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} }, "xdg-basedir": { "version": "4.0.0", diff --git a/package.json b/package.json index 2716484..40f1646 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ "dev-init": "npm run init-server && npm run init-client", "dev-server": "nodemon server.js", "dev-client": "npm start --prefix client", - "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"" + "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"", + "skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\"" }, "author": "", "license": "ISC", "dependencies": { + "@kubernetes/client-node": "^0.15.0", "@types/express": "^4.17.11", "axios": "^0.21.1", "colors": "^1.4.0", diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 0000000..e11f8fd --- /dev/null +++ b/skaffold.yaml @@ -0,0 +1,65 @@ +apiVersion: skaffold/v2beta20 +kind: Config +metadata: + name: flame +build: + artifacts: + - image: shokohsc/flame + context: . + sync: + manual: + - src: controllers/*.js + dest: . + docker: + dockerfile: Dockerfile.dev +deploy: + kustomize: + paths: + - k8s/base +profiles: +- name: dev + activation: + - command: dev + build: + artifacts: + - image: shokohsc/flame + sync: + manual: + - src: controllers/*.js + dest: . + docker: + dockerfile: Dockerfile.dev +- name: shokohsc + build: + artifacts: + - image: shokohsc/flame + sync: + manual: + - src: controllers/*.js + dest: . + kaniko: + dockerfile: Dockerfile.dev + cache: + repo: shokohsc/flame + cluster: + dockerConfig: + secretName: kaniko-secret + namespace: kaniko + pullSecretName: kaniko-secret + deploy: + kustomize: + paths: + - k8s/overlays/shokohsc +- name: prod + build: + artifacts: + - image: shokohsc/flame + kaniko: + dockerfile: Dockerfile + cache: + repo: shokohsc/flame + cluster: + dockerConfig: + secretName: kaniko-secret + namespace: kaniko + pullSecretName: kaniko-secret diff --git a/utils/initialConfig.json b/utils/initialConfig.json index 99bbb46..31306fa 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -68,6 +68,10 @@ "key": "dockerApps", "value": false }, + { + "key": "kubernetesApps", + "value": false + }, { "key": "unpinStoppedApps", "value": false From 5cef34a467f7321e521914455c7b3f454fb8a322 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 17 Aug 2021 10:38:16 +0200 Subject: [PATCH 014/166] Pushed version 1.6.4 --- CHANGELOG.md | 3 +++ client/.env | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e269e15..5c0cb50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.6.4 (2021-08-17) +- Added Kubernetes integration ([#72 continued](https://github.com/pawelmalak/flame/issues/73)) + ### v1.6.3 (2021-08-09) - Added support for custom SVG icons ([#73](https://github.com/pawelmalak/flame/issues/73)) - Added Deezer and Tidal to search queries diff --git a/client/.env b/client/.env index 3395c3f..34a75d7 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.3 +REACT_APP_VERSION=1.6.4 From 8808f65b47fcaf405f895c596fc5e492f7c4c118 Mon Sep 17 00:00:00 2001 From: pawelmalak Date: Tue, 17 Aug 2021 10:44:12 +0200 Subject: [PATCH 015/166] Fixed typo with latest issues --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0cb50..1d46223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ### v1.6.4 (2021-08-17) -- Added Kubernetes integration ([#72 continued](https://github.com/pawelmalak/flame/issues/73)) +- Added Kubernetes integration ([#72 continued](https://github.com/pawelmalak/flame/issues/72)) ### v1.6.3 (2021-08-09) - Added support for custom SVG icons ([#73](https://github.com/pawelmalak/flame/issues/73)) From 45fb337c87b16324115d8a32c4a6bfe209c16b81 Mon Sep 17 00:00:00 2001 From: Samuel Martineau Date: Sat, 21 Aug 2021 19:08:40 +0000 Subject: [PATCH 016/166] Add support for all protocols for urls (#74) --- client/src/utility/urlParser.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/client/src/utility/urlParser.ts b/client/src/utility/urlParser.ts index 87edb63..666935f 100644 --- a/client/src/utility/urlParser.ts +++ b/client/src/utility/urlParser.ts @@ -1,24 +1,23 @@ -export const urlParser = (url: string): string[] => { - let parsedUrl: string; - let displayUrl: string; +const hasProtocol = (url: string): boolean => /^\w+:\/\//.test(url); +const isSteamUrl = (url: string): boolean => /^steam:\/\//.test(url); +const isWebUrl = (url: string): boolean => /^https?:\/\//.test(url); - if (/(https?|steam):\/\//.test(url)) { - // Url starts with http[s]:// or steam:// -> leave it as it is - parsedUrl = url; - } else { +export const urlParser = (url: string): string[] => { + if (!hasProtocol(url)) { // No protocol -> apply http:// prefix - parsedUrl = `http://${url}`; + url = `http://${url}`; } // Create simplified url to display as text - if (/steam:\/\//.test(url)) { + let displayUrl: string; + if (isSteamUrl(url)) { displayUrl = 'Run Steam App'; - } else { + } else if (isWebUrl(url)) { displayUrl = url - .replace(/https?:\/\//, '') - .replace('www.', '') - .replace(/\/$/, ''); - } - - return [displayUrl, parsedUrl] -} \ No newline at end of file + .replace(/https?:\/\//, '') + .replace('www.', '') + .replace(/\/$/, ''); + } else displayUrl = url; + + return [displayUrl, url]; +}; From 85219957581adfd0875c132c9a82fec9c6065552 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 28 Aug 2021 11:42:54 +0200 Subject: [PATCH 017/166] Pushed version 1.6.5 --- CHANGELOG.md | 3 +++ client/.env | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d46223..e40a21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.6.5 (2021-08-28) +- Added support for more URL schemes ([#74](https://github.com/pawelmalak/flame/issues/74)) + ### v1.6.4 (2021-08-17) - Added Kubernetes integration ([#72 continued](https://github.com/pawelmalak/flame/issues/72)) diff --git a/client/.env b/client/.env index 34a75d7..df82210 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.4 +REACT_APP_VERSION=1.6.5 From 6ae6c58f4ca36a8c86d2ed9fc5450effc2c2937f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Sep 2021 12:24:01 +0200 Subject: [PATCH 018/166] Local search for apps --- README.md | 2 +- .../src/components/Apps/AppGrid/AppGrid.tsx | 34 +- client/src/components/Apps/Apps.tsx | 69 +- client/src/components/Home/Home.tsx | 138 +- client/src/components/SearchBar/SearchBar.tsx | 27 +- .../Settings/OtherSettings/OtherSettings.tsx | 152 +- client/src/interfaces/SearchResult.ts | 5 + client/src/interfaces/index.ts | 3 +- client/src/utility/searchParser.ts | 41 +- client/src/utility/searchQueries.json | 5 + package-lock.json | 4536 +---------------- package.json | 14 +- utils/initConfig.js | 20 +- utils/initialConfig.json | 2 +- 14 files changed, 297 insertions(+), 4751 deletions(-) create mode 100644 client/src/interfaces/SearchResult.ts diff --git a/README.md b/README.md index dadb428..669b7b0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ git clone https://github.com/pawelmalak/flame cd flame # run only once -npm run dev-init +npm run dev:init # start backend and frontend development servers npm run dev diff --git a/client/src/components/Apps/AppGrid/AppGrid.tsx b/client/src/components/Apps/AppGrid/AppGrid.tsx index cacc19c..30d5c8c 100644 --- a/client/src/components/Apps/AppGrid/AppGrid.tsx +++ b/client/src/components/Apps/AppGrid/AppGrid.tsx @@ -7,6 +7,7 @@ import AppCard from '../AppCard/AppCard'; interface ComponentProps { apps: App[]; totalApps?: number; + searching: boolean; } const AppGrid = (props: ComponentProps): JSX.Element => { @@ -16,26 +17,37 @@ const AppGrid = (props: ComponentProps): JSX.Element => { apps = (
{props.apps.map((app: App): JSX.Element => { - return + return ; })}
- ) + ); } else { if (props.totalApps) { - apps = ( -

There are no pinned applications. You can pin them from the /applications menu

- ); + if (props.searching) { + apps = ( +

+ No apps match your search criteria +

+ ); + } else { + apps = ( +

+ There are no pinned applications. You can pin them from the{' '} + /applications menu +

+ ); + } } else { apps = ( -

You don't have any applications. You can add a new one from /applications menu

+

+ You don't have any applications. You can add a new one from{' '} + /applications menu +

); } } return apps; -} +}; -export default AppGrid; \ No newline at end of file +export default AppGrid; diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 88c3fff..751a196 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -27,14 +27,11 @@ interface ComponentProps { getApps: Function; apps: App[]; loading: boolean; + searching: boolean; } const Apps = (props: ComponentProps): JSX.Element => { - const { - getApps, - apps, - loading - } = props; + const { getApps, apps, loading, searching = false } = props; const [modalIsOpen, setModalIsOpen] = useState(false); const [isInEdit, setIsInEdit] = useState(false); @@ -47,8 +44,8 @@ const Apps = (props: ComponentProps): JSX.Element => { orderId: 0, id: 0, createdAt: new Date(), - updatedAt: new Date() - }) + updatedAt: new Date(), + }); useEffect(() => { if (apps.length === 0) { @@ -59,63 +56,57 @@ const Apps = (props: ComponentProps): JSX.Element => { const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); setIsInUpdate(false); - } + }; const toggleEdit = (): void => { setIsInEdit(!isInEdit); setIsInUpdate(false); - } + }; const toggleUpdate = (app: App): void => { setAppInUpdate(app); setIsInUpdate(true); setModalIsOpen(true); - } + }; return ( - {!isInUpdate - ? - : - } + {!isInUpdate ? ( + + ) : ( + + )} Go back)} + title="All Applications" + subtitle={Go back} /> - +
- - + +
- {loading - ? - : (!isInEdit - ? - : ) - } + {loading ? ( + + ) : !isInEdit ? ( + + ) : ( + + )}
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { apps: state.app.apps, - loading: state.app.loading - } -} + loading: state.app.loading, + }; +}; -export default connect(mapStateToProps, { getApps })(Apps); \ No newline at end of file +export default connect(mapStateToProps, { getApps })(Apps); diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index ece4a8a..12097bf 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -47,13 +47,16 @@ const Home = (props: ComponentProps): JSX.Element => { appsLoading, getCategories, categories, - categoriesLoading + categoriesLoading, } = props; const [header, setHeader] = useState({ dateTime: dateTime(), - greeting: greeter() - }) + greeting: greeter(), + }); + + // Local search query + const [localSearch, setLocalSearch] = useState(null); // Load applications useEffect(() => { @@ -78,78 +81,93 @@ const Home = (props: ComponentProps): JSX.Element => { interval = setInterval(() => { setHeader({ dateTime: dateTime(), - greeting: greeter() - }) + greeting: greeter(), + }); }, 1000); } return () => clearInterval(interval); - }, []) - + }, []); + return ( - {searchConfig('hideSearch', 0) !== 1 - ? - :
- } + {searchConfig('hideSearch', 0) !== 1 ? ( + + ) : ( +
+ )} - {searchConfig('hideHeader', 0) !== 1 - ? ( -
-

{header.dateTime}

- Go to Settings - -

{header.greeting}

- -
-
- ) - :
- } - - {searchConfig('hideApps', 0) !== 1 - ? ( - - {appsLoading - ? - : app.isPinned)} - totalApps={apps.length} - /> - } -
-
) - :
- } + {searchConfig('hideHeader', 0) !== 1 ? ( +
+

{header.dateTime}

+ + Go to Settings + + +

{header.greeting}

+ +
+
+ ) : ( +
+ )} - {searchConfig('hideCategories', 0) !== 1 - ? ( - - {categoriesLoading - ? - : category.isPinned)} - totalCategories={categories.length} - /> - } - ) - :
- } + {searchConfig('hideApps', 0) !== 1 ? ( + + + {appsLoading ? ( + + ) : ( + isPinned) + : apps.filter(({ name }) => + new RegExp(localSearch, 'i').test(name) + ) + } + totalApps={apps.length} + searching={!!localSearch} + /> + )} +
+
+ ) : ( +
+ )} - - + {searchConfig('hideCategories', 0) !== 1 ? ( + + + {categoriesLoading ? ( + + ) : ( + category.isPinned + )} + totalCategories={categories.length} + /> + )} + + ) : ( +
+ )} + + +
- ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { appsLoading: state.app.loading, apps: state.app.apps, categoriesLoading: state.bookmark.loading, - categories: state.bookmark.categories - } -} + categories: state.bookmark.categories, + }; +}; -export default connect(mapStateToProps, { getApps, getCategories })(Home); \ No newline at end of file +export default connect(mapStateToProps, { getApps, getCategories })(Home); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 029f175..0b5f3cc 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -15,36 +15,41 @@ import { searchParser } from '../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; + setLocalSearch: (query: string) => void; } const SearchBar = (props: ComponentProps): JSX.Element => { + const { setLocalSearch, createNotification } = props; + const inputRef = useRef(document.createElement('input')); useEffect(() => { inputRef.current.focus(); - }, []) + }, []); const searchHandler = (e: KeyboardEvent) => { if (e.code === 'Enter') { - const prefixFound = searchParser(inputRef.current.value); + const searchResult = searchParser(inputRef.current.value); - if (!prefixFound) { - props.createNotification({ + if (!searchResult.prefix) { + createNotification({ title: 'Error', - message: 'Prefix not found' - }) + message: 'Prefix not found', + }); + } else if (searchResult.isLocal) { + setLocalSearch(searchResult.query); } } - } + }; return ( searchHandler(e)} /> - ) -} + ); +}; -export default connect(null, { createNotification })(SearchBar); \ No newline at end of file +export default connect(null, { createNotification })(SearchBar); diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 31bbd52..afaf072 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -6,7 +6,7 @@ import { createNotification, updateConfig, sortApps, - sortCategories + sortCategories, } from '../../../store/actions'; // Typescript @@ -14,7 +14,7 @@ import { GlobalState, NewNotification, Query, - SettingsForm + SettingsForm, } from '../../../interfaces'; // UI @@ -53,7 +53,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { searchSameTab: 0, dockerApps: 1, kubernetesApps: 1, - unpinStoppedApps: 1 + unpinStoppedApps: 1, }); // Get config @@ -73,7 +73,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { searchSameTab: searchConfig('searchSameTab', 0), dockerApps: searchConfig('dockerApps', 0), kubernetesApps: searchConfig('kubernetesApps', 0), - unpinStoppedApps: searchConfig('unpinStoppedApps', 0) + unpinStoppedApps: searchConfig('unpinStoppedApps', 0), }); }, [props.loading]); @@ -105,115 +105,117 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { setFormData({ ...formData, - [e.target.name]: value + [e.target.name]: value, }); }; return ( -
formSubmitHandler(e)}> + formSubmitHandler(e)}> {/* OTHER OPTIONS */}

Miscellaneous

- + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {/* BEAHVIOR OPTIONS */}

App Behavior

- - - + - + - - + - + inputChangeHandler(e, true)} + onChange={(e) => inputChangeHandler(e, true)} > - + - + - + inputChangeHandler(e, true)} + onChange={(e) => inputChangeHandler(e, true)} > - - - - - - - - - inputChangeHandler(e, true)} - > - - - - inputChangeHandler(e)} + > + {queries.map((query: Query, idx) => ( + + ))} + + + + + + + + + + + +
+ ); +}; + +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.config.loading, + }; +}; + +const actions = { + createNotification, + updateConfig, +}; + +export default connect(mapStateToProps, actions)(SearchSettings); diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index b1eb300..5df8ec6 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,73 +1,61 @@ +// import { NavLink, Link, Switch, Route } from 'react-router-dom'; +// Typescript +import { Route as SettingsRoute } from '../../interfaces'; + +// CSS import classes from './Settings.module.css'; -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; - +// Components import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; import AppDetails from './AppDetails/AppDetails'; import StyleSettings from './StyleSettings/StyleSettings'; +import SearchSettings from './SearchSettings/SearchSettings'; + +// UI +import { Container } from '../UI/Layout/Layout'; +import Headline from '../UI/Headlines/Headline/Headline'; + +// Data +import { routes } from './settings.json'; const Settings = (): JSX.Element => { return ( - Go back} - /> + Go back} />
+ {/* NAVIGATION MENU */} + + {/* ROUTES */}
- - - - - + + + + + +
- ) -} + ); +}; -export default Settings; \ No newline at end of file +export default Settings; diff --git a/client/src/components/Settings/settings.json b/client/src/components/Settings/settings.json new file mode 100644 index 0000000..3cc24e9 --- /dev/null +++ b/client/src/components/Settings/settings.json @@ -0,0 +1,28 @@ +{ + "routes": [ + { + "name": "Theme", + "dest": "/settings" + }, + { + "name": "Weather", + "dest": "/settings/weather" + }, + { + "name": "Search", + "dest": "/settings/search" + }, + { + "name": "Other", + "dest": "/settings/other" + }, + { + "name": "CSS", + "dest": "/settings/css" + }, + { + "name": "App", + "dest": "/settings/app" + } + ] +} diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 177821d..22856be 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -5,6 +5,12 @@ export interface WeatherForm { isCelsius: number; } +export interface SearchForm { + hideSearch: number; + defaultSearchProvider: string; + searchSameTab: number; +} + export interface SettingsForm { customTitle: string; pinAppsByDefault: number; @@ -12,12 +18,12 @@ export interface SettingsForm { hideHeader: number; hideApps: number; hideCategories: number; - hideSearch: number; - defaultSearchProvider: string; + // hideSearch: number; + // defaultSearchProvider: string; useOrdering: string; appsSameTab: number; bookmarksSameTab: number; - searchSameTab: number; + // searchSameTab: number; dockerApps: number; kubernetesApps: number; unpinStoppedApps: number; diff --git a/client/src/interfaces/Route.ts b/client/src/interfaces/Route.ts new file mode 100644 index 0000000..9d571dd --- /dev/null +++ b/client/src/interfaces/Route.ts @@ -0,0 +1,4 @@ +export interface Route { + name: string; + dest: string; +} diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 6892fb5..b9683dd 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -10,3 +10,4 @@ export * from './Config'; export * from './Forms'; export * from './Query'; export * from './SearchResult'; +export * from './Route'; From 84bd641cf2b405a17e21d94c27b7ebc3f8a545fa Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 12:29:17 +0200 Subject: [PATCH 026/166] Database migrations --- CHANGELOG.md | 3 + client/.env | 2 +- db.js | 32 ------ db/index.js | 51 ++++++++++ db/migrations/00_initial.js | 189 ++++++++++++++++++++++++++++++++++++ models/Config.js | 44 +++++---- package-lock.json | 13 +++ package.json | 1 + 8 files changed, 282 insertions(+), 53 deletions(-) delete mode 100644 db.js create mode 100644 db/index.js create mode 100644 db/migrations/00_initial.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a372a7..0ab36d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.6.8 (2021-10-05) +- Implemented migration system for database + ### v1.6.7 (2021-10-04) - Add multiple labels to Docker Compose ([#90](https://github.com/pawelmalak/flame/issues/90)) - Custom icons via Docker Compose labels ([#91](https://github.com/pawelmalak/flame/issues/91)) diff --git a/client/.env b/client/.env index 482555e..afab507 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.7 \ No newline at end of file +REACT_APP_VERSION=1.6.8 \ No newline at end of file diff --git a/db.js b/db.js deleted file mode 100644 index 6c1eb7c..0000000 --- a/db.js +++ /dev/null @@ -1,32 +0,0 @@ -const { Sequelize } = require('sequelize'); -const Logger = require('./utils/Logger'); -const logger = new Logger(); - -const sequelize = new Sequelize({ - dialect: 'sqlite', - storage: './data/db.sqlite', - logging: false, -}); - -const connectDB = async () => { - try { - await sequelize.authenticate(); - logger.log('Connected to database'); - - const syncModels = true; - - if (syncModels) { - logger.log('Starting model synchronization'); - await sequelize.sync({ alter: true }); - logger.log('All models were synchronized'); - } - } catch (error) { - logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR'); - process.exit(1); - } -}; - -module.exports = { - connectDB, - sequelize, -}; diff --git a/db/index.js b/db/index.js new file mode 100644 index 0000000..fbff66d --- /dev/null +++ b/db/index.js @@ -0,0 +1,51 @@ +const { Sequelize } = require('sequelize'); +const { join } = require('path'); +const fs = require('fs'); +const Umzug = require('umzug'); + +const Logger = require('../utils/Logger'); +const logger = new Logger(); + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: './data/db.sqlite', + logging: false, +}); + +const umzug = new Umzug({ + migrations: { + path: join(__dirname, './migrations'), + params: [sequelize.getQueryInterface()], + }, + storage: 'sequelize', + storageOptions: { + sequelize, + }, +}); + +const connectDB = async () => { + try { + if (fs.existsSync('data/db.sqlite')) { + fs.copyFileSync('data/db.sqlite', 'data/backup_db.sqlite'); + } + + await sequelize.authenticate(); + logger.log('Connected to database'); + + // migrations + const pendingMigrations = await umzug.pending(); + + if (pendingMigrations.length > 0) { + logger.log('Executing pending migrations'); + await umzug.up(); + } + } catch (error) { + logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR'); + process.exit(1); + } +}; + +module.exports = { + connectDB, + sequelize, +}; diff --git a/db/migrations/00_initial.js b/db/migrations/00_initial.js new file mode 100644 index 0000000..e6d7b13 --- /dev/null +++ b/db/migrations/00_initial.js @@ -0,0 +1,189 @@ +const { DataTypes } = require('sequelize'); +const { INTEGER, DATE, STRING, TINYINT, FLOAT, TEXT } = DataTypes; + +const up = async (query) => { + // CONFIG TABLE + await query.createTable('config', { + id: { + type: INTEGER, + autoIncrement: true, + primaryKey: true, + }, + key: { + type: STRING, + allowNull: false, + unique: true, + }, + value: { + type: STRING, + allowNull: false, + }, + valueType: { + type: STRING, + allowNull: false, + }, + isLocked: { + type: TINYINT, + defaultValue: 0, + }, + createdAt: { + type: DATE, + allowNull: false, + }, + updatedAt: { + type: DATE, + allowNull: false, + }, + }); + + // WEATHER TABLE + await query.createTable('weather', { + id: { + type: INTEGER, + autoIncrement: true, + primaryKey: true, + }, + externalLastUpdate: { + type: STRING, + }, + tempC: { + type: FLOAT, + }, + tempF: { + type: FLOAT, + }, + isDay: { + type: INTEGER, + }, + cloud: { + type: INTEGER, + }, + conditionText: { + type: TEXT, + }, + conditionCode: { + type: INTEGER, + }, + createdAt: { + type: DATE, + allowNull: false, + }, + updatedAt: { + type: DATE, + allowNull: false, + }, + }); + + // CATEGORIES TABLE + await query.createTable('categories', { + id: { + type: INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: STRING, + allowNull: false, + }, + isPinned: { + type: TINYINT, + defaultValue: 0, + }, + createdAt: { + type: DATE, + allowNull: false, + }, + updatedAt: { + type: DATE, + allowNull: false, + }, + orderId: { + type: INTEGER, + defaultValue: null, + }, + }); + + // BOOKMARKS TABLE + await query.createTable('bookmarks', { + id: { + type: INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: STRING, + allowNull: false, + }, + url: { + type: STRING, + allowNull: false, + }, + categoryId: { + type: INTEGER, + allowNull: false, + }, + icon: { + type: STRING, + defaultValue: '', + }, + createdAt: { + type: DATE, + allowNull: false, + }, + updatedAt: { + type: DATE, + allowNull: false, + }, + }); + + // APPS TABLE + await query.createTable('apps', { + id: { + type: INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: STRING, + allowNull: false, + }, + url: { + type: STRING, + allowNull: false, + }, + icon: { + type: STRING, + allowNull: false, + defaultValue: 'cancel', + }, + isPinned: { + type: TINYINT, + defaultValue: 0, + }, + createdAt: { + type: DATE, + allowNull: false, + }, + updatedAt: { + type: DATE, + allowNull: false, + }, + orderId: { + type: INTEGER, + defaultValue: null, + }, + }); +}; + +const down = async (query) => { + await query.dropTable('config'); + await query.dropTable('weather'); + await query.dropTable('categories'); + await query.dropTable('bookmarks'); + await query.dropTable('apps'); +}; + +module.exports = { + up, + down, +}; diff --git a/models/Config.js b/models/Config.js index 3919f3f..675aaa8 100644 --- a/models/Config.js +++ b/models/Config.js @@ -1,26 +1,30 @@ const { DataTypes } = require('sequelize'); const { sequelize } = require('../db'); -const Config = sequelize.define('Config', { - key: { - type: DataTypes.STRING, - allowNull: false, - unique: true +const Config = sequelize.define( + 'Config', + { + key: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + value: { + type: DataTypes.STRING, + allowNull: false, + }, + valueType: { + type: DataTypes.STRING, + allowNull: false, + }, + isLocked: { + type: DataTypes.TINYINT, + defaultValue: 0, + }, }, - value: { - type: DataTypes.STRING, - allowNull: false - }, - valueType: { - type: DataTypes.STRING, - allowNull: false - }, - isLocked: { - type: DataTypes.BOOLEAN, - defaultValue: false + { + tableName: 'config', } -}, { - tableName: 'config' -}); +); -module.exports = Config; \ No newline at end of file +module.exports = Config; diff --git a/package-lock.json b/package-lock.json index a7183e8..dab2464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -525,6 +525,11 @@ "inherits": "~2.0.0" } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -3240,6 +3245,14 @@ "is-typedarray": "^1.0.0" } }, + "umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "requires": { + "bluebird": "^3.7.2" + } + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", diff --git a/package.json b/package.json index 40f1646..dbeb5ee 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "node-schedule": "^2.0.0", "sequelize": "^6.6.2", "sqlite3": "^5.0.2", + "umzug": "^2.3.0", "ws": "^7.4.6" }, "devDependencies": { From 59271d33764abbc1a0b12f7ee29194ad7a5a66a2 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 13:17:09 +0200 Subject: [PATCH 027/166] Create database backup before migrating --- .env | 3 ++- db/index.js | 5 ++--- db/utils/backupDb.js | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 db/utils/backupDb.js diff --git a/.env b/.env index f1644a5..de0f091 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ PORT=5005 -NODE_ENV=development \ No newline at end of file +NODE_ENV=development +VERSION=1.6.8 \ No newline at end of file diff --git a/db/index.js b/db/index.js index fbff66d..34e715f 100644 --- a/db/index.js +++ b/db/index.js @@ -2,6 +2,7 @@ const { Sequelize } = require('sequelize'); const { join } = require('path'); const fs = require('fs'); const Umzug = require('umzug'); +const backupDB = require('./utils/backupDb'); const Logger = require('../utils/Logger'); const logger = new Logger(); @@ -25,9 +26,7 @@ const umzug = new Umzug({ const connectDB = async () => { try { - if (fs.existsSync('data/db.sqlite')) { - fs.copyFileSync('data/db.sqlite', 'data/backup_db.sqlite'); - } + backupDB(); await sequelize.authenticate(); logger.log('Connected to database'); diff --git a/db/utils/backupDb.js b/db/utils/backupDb.js new file mode 100644 index 0000000..572679b --- /dev/null +++ b/db/utils/backupDb.js @@ -0,0 +1,21 @@ +const fs = require('fs'); + +const backupDB = () => { + if (!fs.existsSync('data/db_backups')) { + fs.mkdirSync('data/db_backups'); + } + + const version = process.env.VERSION; + const slug = `db-${version.replace(/\./g, '')}-backup.sqlite`; + + const srcPath = 'data/db.sqlite'; + const destPath = `data/db_backups/${slug}`; + + if (fs.existsSync(srcPath)) { + if (!fs.existsSync(destPath)) { + fs.copyFileSync(srcPath, destPath); + } + } +}; + +module.exports = backupDB; From bf1aa9e85cc9e3df825a86e99ab0019ec6c6cefe Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 16:31:56 +0200 Subject: [PATCH 028/166] Clickable notifications with url redirect --- CHANGELOG.md | 3 ++ .../NotificationCenter/NotificationCenter.tsx | 15 +++++----- .../UI/Notification/Notification.tsx | 24 ++++++++++----- client/src/interfaces/Notification.ts | 3 +- client/src/utility/checkVersion.ts | 29 ++++++++++++------- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a372a7..4d81511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.7.0 (TBA) +- URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86)) + ### v1.6.7 (2021-10-04) - Add multiple labels to Docker Compose ([#90](https://github.com/pawelmalak/flame/issues/90)) - Custom icons via Docker Compose labels ([#91](https://github.com/pawelmalak/flame/issues/91)) diff --git a/client/src/components/NotificationCenter/NotificationCenter.tsx b/client/src/components/NotificationCenter/NotificationCenter.tsx index 29c9cb2..733316b 100644 --- a/client/src/components/NotificationCenter/NotificationCenter.tsx +++ b/client/src/components/NotificationCenter/NotificationCenter.tsx @@ -20,19 +20,20 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => { - ) + ); })} - ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { - notifications: state.notification.notifications - } -} + notifications: state.notification.notifications, + }; +}; -export default connect(mapStateToProps)(NotificationCenter); \ No newline at end of file +export default connect(mapStateToProps)(NotificationCenter); diff --git a/client/src/components/UI/Notification/Notification.tsx b/client/src/components/UI/Notification/Notification.tsx index 95109e1..2bd5185 100644 --- a/client/src/components/UI/Notification/Notification.tsx +++ b/client/src/components/UI/Notification/Notification.tsx @@ -8,12 +8,16 @@ interface ComponentProps { title: string; message: string; id: number; + url: string | null; clearNotification: (id: number) => void; } const Notification = (props: ComponentProps): JSX.Element => { const [isOpen, setIsOpen] = useState(true); - const elementClasses = [classes.Notification, isOpen ? classes.NotificationOpen : classes.NotificationClose].join(' '); + const elementClasses = [ + classes.Notification, + isOpen ? classes.NotificationOpen : classes.NotificationClose, + ].join(' '); useEffect(() => { const closeNotification = setTimeout(() => { @@ -22,21 +26,27 @@ const Notification = (props: ComponentProps): JSX.Element => { const clearNotification = setTimeout(() => { props.clearNotification(props.id); - }, 3600) + }, 3600); return () => { window.clearTimeout(closeNotification); window.clearTimeout(clearNotification); + }; + }, []); + + const clickHandler = () => { + if (props.url) { + window.open(props.url, '_blank'); } - }, []) + }; return ( -
+

{props.title}

{props.message}

- ) -} + ); +}; -export default connect(null, { clearNotification })(Notification); \ No newline at end of file +export default connect(null, { clearNotification })(Notification); diff --git a/client/src/interfaces/Notification.ts b/client/src/interfaces/Notification.ts index 80a49f2..5054922 100644 --- a/client/src/interfaces/Notification.ts +++ b/client/src/interfaces/Notification.ts @@ -1,8 +1,9 @@ export interface NewNotification { title: string; message: string; + url?: string; } export interface Notification extends NewNotification { id: number; -} \ No newline at end of file +} diff --git a/client/src/utility/checkVersion.ts b/client/src/utility/checkVersion.ts index e1a0508..d4cdb9a 100644 --- a/client/src/utility/checkVersion.ts +++ b/client/src/utility/checkVersion.ts @@ -4,24 +4,31 @@ import { createNotification } from '../store/actions'; export const checkVersion = async (isForced: boolean = false) => { try { - const res = await axios.get('https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env'); + const res = await axios.get( + 'https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env' + ); const githubVersion = res.data .split('\n') - .map(pair => pair.split('='))[0][1]; + .map((pair) => pair.split('='))[0][1]; if (githubVersion !== process.env.REACT_APP_VERSION) { - store.dispatch(createNotification({ - title: 'Info', - message: 'New version is available!' - })) + store.dispatch( + createNotification({ + title: 'Info', + message: 'New version is available!', + url: 'https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md', + }) + ); } else if (isForced) { - store.dispatch(createNotification({ - title: 'Info', - message: 'You are using the latest version!' - })) + store.dispatch( + createNotification({ + title: 'Info', + message: 'You are using the latest version!', + }) + ); } } catch (err) { console.log(err); } -} \ No newline at end of file +}; From 084218027c93976da701493be164e13150dfee8d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Oct 2021 17:08:37 +0200 Subject: [PATCH 029/166] Bugfix for #83 --- CHANGELOG.md | 1 + .../src/components/Apps/AppForm/AppForm.tsx | 74 ++++----- .../Bookmarks/BookmarkForm/BookmarkForm.tsx | 145 ++++++++++-------- 3 files changed, 117 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d81511..9384559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### v1.7.0 (TBA) +- Fixed bug related to creating new apps/bookmarks with custom icon ([#83](https://github.com/pawelmalak/flame/issues/83)) - URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86)) ### v1.6.7 (2021-10-04) diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 5d05f0a..d44418e 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -22,7 +22,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const [formData, setFormData] = useState({ name: '', url: '', - icon: '' + icon: '', }); useEffect(() => { @@ -30,13 +30,13 @@ const AppForm = (props: ComponentProps): JSX.Element => { setFormData({ name: props.app.name, url: props.app.url, - icon: props.app.icon + icon: props.app.icon, }); } else { setFormData({ name: '', url: '', - icon: '' + icon: '', }); } }, [props.app]); @@ -44,7 +44,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - [e.target.name]: e.target.value + [e.target.name]: e.target.value, }); }; @@ -59,6 +59,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const createFormData = (): FormData => { const data = new FormData(); + if (customIcon) { data.append('icon', customIcon); } @@ -88,10 +89,8 @@ const AppForm = (props: ComponentProps): JSX.Element => { setFormData({ name: '', url: '', - icon: '' + icon: '', }); - - setCustomIcon(null); }; return ( @@ -100,33 +99,33 @@ const AppForm = (props: ComponentProps): JSX.Element => { formHandler={formSubmitHandler} > - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -136,19 +135,19 @@ const AppForm = (props: ComponentProps): JSX.Element => { {!useCustomIcon ? ( // use mdi icon - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -163,17 +162,20 @@ const AppForm = (props: ComponentProps): JSX.Element => { ) : ( // upload custom icon - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> toggleUseCustomIcon(!useCustomIcon)} + onClick={() => { + setCustomIcon(null); + toggleUseCustomIcon(!useCustomIcon); + }} className={classes.Switch} > Switch to MDI diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 10d6de2..5162c89 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -1,32 +1,40 @@ +// React import { useState, SyntheticEvent, Fragment, ChangeEvent, - useEffect + useEffect, } from 'react'; -import { connect } from 'react-redux'; -import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import { - Bookmark, - Category, - GlobalState, - NewBookmark, - NewCategory, - NewNotification -} from '../../../interfaces'; -import { ContentType } from '../Bookmarks'; +// Redux +import { connect } from 'react-redux'; import { getCategories, addCategory, addBookmark, updateCategory, updateBookmark, - createNotification + createNotification, } from '../../../store/actions'; + +// Typescript +import { + Bookmark, + Category, + GlobalState, + NewBookmark, + NewCategory, + NewNotification, +} from '../../../interfaces'; +import { ContentType } from '../Bookmarks'; + +// UI +import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; + +// CSS import classes from './BookmarkForm.module.css'; interface ComponentProps { @@ -53,14 +61,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); const [categoryName, setCategoryName] = useState({ - name: '' + name: '', }); const [formData, setFormData] = useState({ name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); // Load category data if provided for editing @@ -79,14 +87,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: props.bookmark.name, url: props.bookmark.url, categoryId: props.bookmark.categoryId, - icon: props.bookmark.icon + icon: props.bookmark.icon, }); } else { setFormData({ name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); } }, [props.bookmark]); @@ -117,7 +125,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { if (formData.categoryId === -1) { props.createNotification({ title: 'Error', - message: 'Please select category' + message: 'Please select category', }); return; } @@ -133,10 +141,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: '', url: '', categoryId: formData.categoryId, - icon: '' + icon: '', }); - setCustomIcon(null); + // setCustomIcon(null); } } else { // Update @@ -150,12 +158,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const data = createFormData(); props.updateBookmark(props.bookmark.id, data, { prev: props.bookmark.categoryId, - curr: formData.categoryId + curr: formData.categoryId, }); } else { props.updateBookmark(props.bookmark.id, formData, { prev: props.bookmark.categoryId, - curr: formData.categoryId + curr: formData.categoryId, }); } @@ -163,7 +171,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); setCustomIcon(null); @@ -176,14 +184,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - [e.target.name]: e.target.value + [e.target.name]: e.target.value, }); }; const selectChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - categoryId: parseInt(e.target.value) + categoryId: parseInt(e.target.value), }); }; @@ -215,48 +223,48 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { {props.contentType === ContentType.category ? ( - + setCategoryName({ name: e.target.value })} + onChange={(e) => setCategoryName({ name: e.target.value })} /> ) : ( - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -264,12 +272,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -311,16 +319,19 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { ) : ( // custom - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + type="file" + name="icon" + id="icon" + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> toggleUseCustomIcon(!useCustomIcon)} + onClick={() => { + setCustomIcon(null); + toggleUseCustomIcon(!useCustomIcon); + }} className={classes.Switch} > Switch to MDI @@ -336,7 +347,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { - categories: state.bookmark.categories + categories: state.bookmark.categories, }; }; @@ -346,7 +357,7 @@ const dispatchMap = { addBookmark, updateCategory, updateBookmark, - createNotification + createNotification, }; export default connect(mapStateToProps, dispatchMap)(BookmarkForm); From a162450568a24cf6c372625ef822968f4021110b Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Oct 2021 11:23:30 +0200 Subject: [PATCH 030/166] Added static fonts --- client/public/index.html | 13 +++-- .../fonts/Roboto/roboto-v29-latin-500.woff | Bin 0 -> 20532 bytes .../fonts/Roboto/roboto-v29-latin-500.woff2 | Bin 0 -> 15920 bytes .../fonts/Roboto/roboto-v29-latin-700.woff | Bin 0 -> 20396 bytes .../fonts/Roboto/roboto-v29-latin-700.woff2 | Bin 0 -> 15828 bytes .../fonts/Roboto/roboto-v29-latin-900.woff | Bin 0 -> 20412 bytes .../fonts/Roboto/roboto-v29-latin-900.woff2 | Bin 0 -> 15724 bytes .../Roboto/roboto-v29-latin-regular.woff | Bin 0 -> 20332 bytes .../Roboto/roboto-v29-latin-regular.woff2 | Bin 0 -> 15688 bytes client/src/index.css | 46 ++++++++++++++++-- 10 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff create mode 100644 client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 diff --git a/client/public/index.html b/client/public/index.html index 3f43c40..c93d95e 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -4,16 +4,15 @@ - - - - + + Flame -
- - \ No newline at end of file + diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff new file mode 100644 index 0000000000000000000000000000000000000000..c9eb5cabfba7d5ff961fce4c067f0d3fae77db11 GIT binary patch literal 20532 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

_ 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 SYT_{{QN5GB$em6A0000dHT^mO literal 0 HcmV?d00001 diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff new file mode 100644 index 0000000000000000000000000000000000000000..a5d98fc6202f5cf5fd8b556ca834e8e9dbaafac1 GIT binary patch literal 20396 zcmYg#V{|566YY~^Vsm2K#>BR5+twtR*tTs=Y}>YzCz#mA&HLT^L!~ zJ@~D|{6-$s2|bCin+qWT0P`IWAn_k)e19~VIhfl50I;Or8kTQ#>fd5lnj1QQ`@(5_ zYvBF|C;+Otji=eSX#xQ75CQ-;y^q3rx)vt39^YYtzBS)5K#&+&NH{D^4Zk%K_um}* ze|TS+*Rl8}0RTzbZ=3KNq_E@=vlg~4-@cME-)o2k0Dd45k4hcd*c*TAq`&6}^8C%i zlmeRfCxD%`8H zz{Z5m0$W#AS5sH*Sms#c==xKhU}{C)N|?Y{@bnfD6&Y3u;IST^0v1Q6i5lEfp>E6D zw9;&Ae|DT^eb!>T(cA_#;$*Y2fM^?KPONts9u-q}8P0HZQKg;kGoy?QIMnI06?xiR zVag~G=lk?GxAfWvi{%wwXh8Hi@yrMVO#ZgNo-`)#$Wdx^A{IhAoI8ek4!*L5rjOkI z*~QRG-pas^i_9jnBq*+dS5g@Amx_m?ljL!DKXnM4sRSj&T%e;M|DAdv$^j)b6jP$Z3Y!&fQ{L%mXo@ zX|axA=d8R^mbJS>x^5qQQQHc;L(ju3v6g5DCfRUUU5aUdE6d!i4DycJ<#GF$`aXwI z#;MFtmO&XN=}8P673acLQuatXqso)T53G1}uK1rHMgnjVL`<~J<;QMy!slomi{33z zX(sj_S>lwCl`p>y$wYrWh}qT4*){9gH9b`QjIW1Hx==PlJ`<&i-^8-6{r5S0XZc-u|6S%?=v~Q$Iu%(n9@Vo>{H4n%7wS3vBMN5ey!ihr ze`~u$M2$vdjYfvC&pVw_lgD4%kww+hX|c`Ldj8+#M1*amN<`#F>WA+@WotCqp;ZoH z*Qr&ZQeC#>&i$`krdP&MM~=&RIq1K&&*b9μ@r2h2zTF5D3Lz~+tNz*d0Io>VN^ zv~8jJE{>VVcDeR92T1P9lGV6vvo2Y1)F{fVk? zw*H3F(~WDa{@pY05W6bO`xkuVw(JklJE{3BAu!0;B7jY$>we&m5v*kv=C6g6*sAtw zYm*1`GqW!~ZIcb#(AuA?e|)BLAgI9&^ieB^q{T0o8vWQfnZKsqa_;|r;XS$&;M?{d z)5aYEE8+3tu{bY3@j`c~+(Tm;{5Fu&_X;nrv2mM}qI=14i`||ivl*>3(OEvFj4ERq zoa;wcTnbbpDlaU(2fQBaWa1jJnuj3DK^2OF*r=b1Y7G6F}w#<}fhxD<}tSyxvevum+z6&G{HDyv?buU!(Q5c5bEZOYZlqad4r*e@f zy~P)E#x^c)NTbZ2GAVIF*om7yE`5)HeU5g)vX4J@dc`z@>sTEj6{@-0ekf}((y}AzE^5LUV{tI)W&Vz$tdJOUP)|wA#g>nyywN zYmsEzq(OJiRq6jws?M!moWXdxGZ>_#8n!4OoJqfRPGQoNViu(~u28u!r@t{D_VT4? zduuS}?McXphmEk81X)Ov9mS>U)4@;}WLBf<$#6|-(5n!cF-2RTT1p_2r}9w};U5@$ zAFGezy{+@@iG>Ch8tF(S#+`0XHFjv8^M&Zp245OR3bwyB7Um5C%lr{>Y(nX4E`+*^U{oV zZR{(Q-I4CVv2bbDza>M$fxeUUoyo|Z#f3olsczDz%7H-ogj9YPVR*AI%=Wx!)Q8rU z0M#CBGeAQB`@`a~t{X4vwk3chY7ad%$pd}BGdY@nrpdAwT3UzeeKq+YdT#RD+GhZLez^hyPU< z6@p(uoP+qDot#pD2SfI6p+RcwomwvyL6x7x zJl<58K^#P@%OI*8C{srQ&BY0#TDofik5ahvi7rSvD2yfO2hy)CAmB{vn~D0GERfMi?mh%c3ZpswCYoh3m+5=f1K9&=#lvUqj=u&NftsB`kmm zm_42*GX`EU3$)YbDXjM69?eoVLHrH!&%5@VN<8DLhB2k(ubA=KzK0$&-i^cnkzHX6 zaQ5Rm*)c|`fIjup42ok=tbp72U(DnvGKFG8OtG0JSN9$Frs`BzVHVOV6spz^?t82< zjesC-w9SWgfxazYzRS3{!NLoGS1(}GvFh?Vx}CN+yCNK-T#Hrm4qCgxrUd}zs<73Kb8?W`g9|eJU~tTXj3lISqm;%` zEPbSsq%1?MX0#f0@j}#hw0}oYCaU7ZepUYO-kSV%1+dS1Z2+hN{{TgRPk*SzDtJoX#hY2%>)pF5dmO-`~sl;hz1~m{{^5yU;@xVN&v_Zy#NG=9RTRJ z_P^&R21NXaAZCd9`dND=u|QN>o8wTrBj5){hd4s>Bcp!VJf3bIKN4&m#7By|Kku&Z z^hfyOCVpjr=dlkTK0{i1L2g^boqWa16J)|&1p)7-wgZW2yG$gZs;C}Bt zwkDsCmX)jz$Q%1V7HP|!qZ!iq$*Z8%jWDoObtjpYm-CE@@k9B2giV~xvqUknu4B^w zoLeF^LwQ$t)uuNi;0va3pic->O?}3-!~`9UkW!mW+-v4(k!eZ_WrZFgm1)D8zFXPv z+Cc$8zrFy79_v-VA$8D3o}N3`wzz+}Z#stN^(sb}QL6Upfq4G}@fM{ed`ua{_df3$ ziE*!-W7Rf#vKC4jRI!$+vqr($yv|MM-hI5jx_VdJoF?)=;iBV~mm$}cey z163kY2l{s^QkaNUd!;+N`~iPso8AQzfgCM#452kDdXP*Y+mN*7Zo5ugyZE^_;MTX` z24X#_*Rxn@&ul3hfFS@I0KMNwy4c7*Sc*DIa*O5h9s(`|(5_B+c|ee~%?_bRvbYhk zCfGE`zYIhYQ=Z7_g+|E#X+;^ijs0k)XdFU}lLs0sO9{Ccjj&0XhoNZd7I|v=O0-Mh zRTxIBP|FttLryfjTd5u#A~HzUJ5E7_)Iv{3LPEzz0wxC~FMaFPXt7;5Qk4T$Skgb! zS_nfFuS6z8(2hp*t1xQoM~q>krmhLbs_CmQkqQn?9a!tqrW>-Bs9gtPG)17} zGvxW7RqcPU911$13P|gukl>lC(gBp|=dB(J*SzD7bF=;-2-K+q1ANz|V1CWGY&?uU z11iqZ7W}YE>PEB}>DCsL)V?kZ%pKy|I=GkZ4bW?IrK9HsOlL2~5K^SkYq9#!wEK%N zOTojL&lGB5rUW|*pZVgtlzM*D4;H6*f718`v)xE9CK?MZ`Fmp8B$Z;#aw1+(mg9;3 z+VQI}1kn`{yrCuRJSGygvZ;teDX6V+8x>fwRRrE1mbK5J!K$?VIh4pe=^eZtB=V0X{Cf+XM7#t>r3YpAhKV*B^o5h&$bTT$rq3`nM)k|H-dv2IYwfCoD zC|yBHn=AH9ogVm!xOx{j^Te9v4%#N5hI+2XRru=r0(Tiz9TM3cXDLvi%~C5z797tq8~=ofx`X zK1tUjBHf_^MMw&)5Xoe1DWpb^_ zS@CoekNal@c1baZoPAo(a@GAt8+QwXWOl<{;fs+`Xxqj2CAOEA?6e&O=rZ_XRoQ>6 z6*2vayefp=E3t+yw?|NO#&jK>tx8U1&(Zt$-sjvDftxaWE(=rjmt$K zMMLGV($6Fj$)YHO0sUIFGn9Q}HmgFa1SMix9&+TxBvZJ6?X13 zYkcK|+uuj>z{FCI4mFs*WPD`+7;1l9iTl3lWRkw~BX9vqm zu>sM`l+8uFv$#}<+xA7sBQ8~xbtVB${4dHbRM16{XJi+hpID}GDrj1n2npxEMr*tT z)DJA`SQCKz66A);k|^87r$LPpB$Ylc@9H^oW=t7-M+b=(Du&gy!y{!ey_`;`Kyg^? z->t?Q5fFEm^5~AG9e<%SShwS(u@h(nf!YZ?X0{|@q3At-IaZ47FYWyrB5$YiMl|r! zcMBs|c1J}5V`Gt@{JwI*;W4g|n*CM{winhw<=SOm;}YRlV*VT;p&%+c@cG%ov`IjG zjBn%p!GW1554jiWYbL|^Clv8JJhDQO?b6(>*nGu;|ChQ)0qG#jo|t4QMv%mb+;oXR z!Dl0Wf%51WO=c|aHtp~9n;X_qa-0&}DZ-gM!l?J02{TC{f(XHVWS|@ylMj{;hqfO& zXBdjIKe_D?0UzkVnAL4uRRHcR^juL8Tp$}^pUz{qLOt%b=uA8fOoDD(wSWwJafO(~ z&-Bsz7_H)08&?h3cf?-G`cC79u|&HZ0v{X@Y;ZCUnooG>QOLRCrawaV>HD%Cl@H7@ z!3jO9oeLA6!7SGZLnJB~JUst85uExa^YWaYJqxna(`n^r?6%0cX_{o0_tkj48m-p4 zxC82YCd~FXHs?=~z?bLX^Lp4rgwBVoSFhGtRq=ut7tD=_lNAvwW|T~c@_uD!my=Cp znk3RN!61<|KqO&H`{zT1h8PG|-atRj1v|GXS+~*4p`q&Y`>0TB?4>ICQy=UJzrucq z1MmA{;`il0v$*wG6_*Clxq+fBKh$Zuj~^0 zVtnhwoEFfue=9d5z=~i`2Vt5M8~Cx{ZoK99oPE$6Tvn+|74X- zQTNQ%js3y|PHNHCQnaL@gJ^IkvG{@VnnsZ178e97x~Hq#1MjQAVZPq5)$s+bE;fqg)B9 zXAe{0&eaLxg47!Zx(!FdB0_)<&jK(f$N`ZC9OHF^#Qu|@2OH(i`eAv`y)=Q*1O{sN zsBc9ZkD%^D;-8O>wmW49VL?k2aTpt|xIZde?{7q{R6S9}>_^Coc2tX#%)(Q7hUsKH zUfb{mSOWShlgD@XeTZBJT1s8zALLY_dqt0*vk|h;eH{?>_;OjeXRjZH>ud;y}~3qNthu_2*Krj~vf_>wKpQ39zSB#*8;8 zconAm-Rw-!X1H}6{w9>>O2WFb_qS&hOM;6yi;}K)H3$G-O7s2SjuIp`9%7(86afeZ zjQ8CXfH9B@<|$s6Vs~P?oLWe`hE^tIL>8tF8#%OAu&5jl*4pjnow|_gCkm9G3zttO z35gVq8BpqjHhDR662@c*BpT^O#vIoIdkEx0C{Ph>85@ z+nG!UQuyg`sdS_RG@VW#@UN+NwY)jO9Yq>`rdz+@SAA|IumhtJib>uK1jKDGd2=nw~fS ztQTKnsben=VJH^kUqiwk7GSrStg)}B;d9v?C^K+)*o%)NT9+p(;lqxcHk44T&F@Wi zf{SS>S0*T|g^)ab9%S^-$b5D}19#m%RrZtXXmj{CRW6_wKVX~DUu0Vi`4;2QR%kI;fJQ+t7o6`Y=8LTD)dB)Z4Rd)9W{;cLRmH+omoBV5ue3J)|j4|%; zX)M&hI z@$q~5-hYzCIkzqjb(wa&c3CY({5C&_J68`GUAk9;&DGz8z81WrJiCe6szDcGm*JFE z`x0f3x7-svvs(T_U>9aQ*&98uHi6S*wf2pjzAikr42s7lqLOA_Xuz>5jDo5-5>;`@ zGg|Le9n_hl;?!|FP{%TdCExW6xDJ0oG=wT z<@Lk~JP95sjAl8p^@Q%SyKw`o4{eO<{Ymm=;rv&pQBBo4udWohJP4<6;z8STn12NZ zqJE9VW-#du%uNMy2}}psj}Dx<56g8Cr!!>Z*YOs-Zd0lI_IfaLNU&%2!*X}QOut1cEN5;;#)s|o?y_iw|Ob=*FF6j)7DuKELzO84!t7|#M z+)m~K2VrLMsDe59mCpH|0##{l5?o=AI{LQWfLJmCjZrkSE(D&BGX+>HcClX93sGxO zcge5mHA&pJqEz;pXkxsGkiC?)yxC!r!2#sFf6Fi$>)ed5`LcMR=e*tH3s5Ul)Ag>f;fXH45P5$*4#OgmFfDe_e^e^C}+znSS`7i=ju3jE6Y}tZI*p!LyQR zo^oWUV}kggGoebF&|bJAL?<{u)|?cOfMXeih;9?YmQ&!S(+s0yY;fvzWgy~CYOomS zRVkSAvYA2Li3A7)B<{-xhr4fBP)Ha8zH>9Vt!}Vrm)nU__y=OS_XKsQ36YSWKV%;} zJs7KZpSaiDuZmONtqN<<##h?9oU+l=Wx&sC2Fl8B`qoq7j!T;3aRyJ+5_CPy!s^^pBEud0EkV@cptP%yPR<YY26i-Ed8%n>zQ9L<%kLU--hL&gEu(?%+?g(U{D>McHC|F7$|ta$-4?gfv)n z^8($9h0eAE62YyPDXNFmI#XkbP~-qqxt-p)OmJhO8LfZ^6JEJ3*;hrOLti+Kszj+; zq7GUSMG4aTX>F6f05sgeHXN9n#`zqDH>zZsw@j6VHqlW96uCO6h+?%@5Om_+0cp}( zt%wU?h>c+pjoI@*LNu5As4W8?v|dP{6=C`{TRP?_t;(1z)3YmuMigSoZ!-2qH=>t> zHMtuk`nwC0-8M?o-h!gm050_TFtWkDeTrcb0T4q-s-SuxgyF=a|nE zq>?W66*gL!lIffom8hDL!gyB#yGq4_6dP(@l(<7NE~N4hF8;9vb5XnX=vE;?;tqSS z7t|jeC=K}tw<^spFt;l0F8J#?YapyMQDbX@7yLRdj|ATb{v}_#U+GlzG2#_r$I^4M zbRE8H(v}ar4%LEGV10KH+`S1RNUM<2>n!M!Jj^WCm{F@Nd$4n&jIa$Q=}>E5jJosa z%%GH>|45MKf_P#mW|e7z=YDNux8KcEnjwjqVUQw+5_O%1M63`cuddL*`LvM&vQkPI z+IjIDXGwb61Qt+4zDW_U#tTQ*dItOH{6`yR?mb5PXQ<8;Pxo>1*_Xfug^p5`MIP;o zs`0h-tw?e z1qwhEAe{`84sFoQ>ZRVk?3i&LDtyw?^aat-C?OlhBR2~y=98K-Mm%o*q>)sFKpB=L zJ!a9L_HFVV;CGh1xqnwoOr-_5#BTaFXYy!3xDt8{illAxIy-!DxexpOqP;px4-_+R zv0kWEd)s-$?_fWC6P5R7hW!)g5%0Y;CpA&51C=ah`E|kNh>xyEIwi>@iQW=EBBd2J z@+tNQhH@#QRo9XNeottiJ>hS5ep- zlfKz=Ypmo=7+Gm+_pvIM*3*mX*kSmF4#63Gj>bHdY-6k%7XvR=;Y6@B5Xn%v^NIYwja<61L`s zmKc(&p;nR0iSY9Pnc#@ia|pAc@SE?sa0rWG4w~6*YpP-U0oC{XtGhq1A1XYSN`=0x z8iK}^@~hEAsr*#I@y?qkLJ)2*n`&kpb}$hP zTlValCPtQuGOaVY!_-iuBn6{1g`RGAzVG;U(6||l^TZiF=enZV31U+e35g^BZJ>#- zYclVZBtkC}pEIc+e!-N6e-Ks{k1Jl{nuaYUI?KG}7|K#f>NbZ;-MMblO{@?HJxe?~ zb77;GD03N>aJ?m|b|%=M5M>taJ6=JJz@n}rj^yA<)fJ(D0tO|mC=~Q40oM(8;8Exw zeM&|YvWmZrtnwO8nW~-pgOQwiL%zlWE(mKlgqfvHC+cu(QBE!w2LuU_am0E-Kg`s% zlw=DFUP=W{CuHlVOo~{r9;C>fB8KWLOeW9mM4DTr_4egTnXS-nnqxcSDNuYY$`w^K zY{;0dqo`3^B)G)mh zs-^#DcOT#uKX$;U+4_ZQvDxf?Cg#&@qQ@yzu7H`H z>hmktorAm*Oo`ye%r-Gc2>b^jy>DOtdbftS)L5QScKZk%+_=~33vqU=&QaG+Ij3NPL-tbMIoNKnvx7q$3d?IOr z)K*koU2y~tx=~B-7i@6x^V)~A8To#d?hMfMS=;aT7$bjv9$L1?q{e6{{mK$A*NPCn z<2Gl94@OejPwsdcBrq7q;-O$Nd~D&|glQT%$f%uLZBsZN%CPnP%YbN@B(;Z7g!U-P zHtVs5glRI9+reo@@-SV$chw)<&;RNW=hw(TotXLz2H*QZG_$`~Adzf=8w~F05W8r} zRD^^kk0(9N(%o4!$Q%PY(K~;Hz4?XwV)sfwQ{p?S%8Ft#7Qw^>ff$=Ew7$ODCU>{%S&KgFIaSb*ZI7EQrc#zGFyOP~ zY&~bF_$SONR=_YVrc;ORsd0<%iaDUXk+Z^s0Qcl zGVem47kuG4zab%E2ZtRbv?stTF*IpY`?smePVLho^ScttuEN!W^WhNEMt&} zJ-QHf5`D+o2fu7%J)go}fU4s?a*B;?AO=S;f>ap184;s8FAr!oe>2jnBv7pM97_@i zkruQ-5SPE){4C%${AZpST{|!y%3t&~@ClARerX%q+Xn=lh>R1arkD1l5TL0-U1|fz zi1Bv?raA6R1(V^)CUZLy19H&h+IqQKyWwc7ok*uSxiR>|MGw(Lz}B>HbVYpk#{N(R zhD20-weH+r?A%c6UxPbg9-iPmm2{Z(T z!6f5L1gjOryYii>2tqZN#VOZ}e@(Sf%Ne293)f`G6_-O~P)6#cQWcH%`K!>DZbO6c z&CdBx7fOW&W~^M1`7&1~d%E!-U1O%dJ3~+&3+s~c_q(jrmG)f3`7L$k>c~(1=7$)K zzK@~`2Zp5aSk6snr}i{Ak2tA4l`mVZ&X;Z=RT7dnWbh}VN2Fr7Ci4Dvk?PD+RP;DW zK1vXdUCbIm!?NX`geVN$>?K_ZsC~Q|ma!w}Fq)OscDo5^Lg&j`a!pe$5LmO-gM~?^ zs0h(o>(HFkGP}cF9Zqk?sGh%2)ndCuA}UmjwV2O6!|@Ikzbp5ve#83BmbOXKG^TGr zJ&MQY`yQ4Birdz74|Y3q^{;kRwG4jMbnCG?Wr^Uzgb(Q-1$)7@r}Fdg=r5e#zYz3Q zAtm~jb>_UdLVXe<>Gq9Ezt(W(S?t4@y647lP;j=mnv5CIK@nT_yr=akIK<%6f8y|8 z;Z%9c)*PYIek8GE&#DP5o#G8hR3c~1qoL9b4MrLWpgEkq3x>8ym^785&#%n`-coCHLQ=6}v|OLHOY`lfYA|JgFUadSP#V}HXK z6KwVdS)6cJEioe=XhqzOOJ%~m@Nq%DX->g<|91__{BcuWtFHiZma|NC7kb|T?C-2Z z?Gv9IJ6Xpo*vENs^{u;KOg+PKx5L+FAfwLP;Gl_l++oS{NhODh(_RDXl&}iFS0NyT zoC!Ij?MkdRp+yvJ#^#KC5P{U;K~E-5`Mt!5S7Mq2t8b;R^@^URUl#NnG*>!!{ zB^htEKweJfsb}+1ZJ70H?iV&>8{uz`7NVHFE4vg3;ad6kR5dL@xP+A`1_6_UDCb%B zkvB^*zSIj7rylS!6lNSO&Xl%HCm)1#YqI!uEWgLHtmYkJpyZS?(gOueW^|i*#{;9b zpgwjdfzXD|Xm|s8=FZta_~5eiwGWeoPC1aLE!IkOJE^JpYuzSe)i5(qL(2r_#yrQ@7Vwt41--m)!#<@_3^ zkeM)X)sVI{Xkvsbzp#u`$SM?wl(@t*Vz6*J`23gO7^qK7Q}IHd)bAjFzYo)4?aK>e zYsJ{AzN<=XB7PO|hNt=h zpLZZ~X(F3bzQ?mepc~$GzVN(Ny%{4gr{i_1ZC~Cma!bpnFNvmwrQs57CsOziLY#Tr z!%>W`_c(ODh1O^KDQ6&CP`{RdAzQ((M;)Gm-_k4;^(hp*784aiBOq#H;uD5!^`IU` zBENZH@hyx7kj|uqiD-kXkYn-DS6De+Fb9R!f^E1muk)VWYW4hnzp2kJr*Y@%a6e78 z@F&=Ut!;8K3i6N?DQriJHg<)AYw$&gX`7#R6!;f%2{xswdCZ@F{o^Vt#Uuo8$A^u-`6fe&Yahoq~GW@K}&}oCGzHRNh(`e7bcsr zKKc{PVfI{I-qQoXm;d5Pr^j<`OrCAOEsBs4l35#KJT%`#DMh1g7l_AlHNry8_A{i8+D^@iCqxh1?+o5%^t?!TYd*d24=*Lc{>cQ(HzOcF- z(}i9L<~>ySU2&}f5_Q}Mo|l4WU^cP)^b_98wMKOa^I^G-ZxQ8uRv^|5o&^5y*m(7L zo9jJK8iIA~f_wD`D{`wweX@j%5{ck8wv6A7SrtrrD!0~Z6^*vpuR|O|Go#fx*_lr+ z!5*6~eMya_@;=RW9uCL*UD^~KPx!M}(_qcF*%Oyibc3ao0MUFjtd$z$og@sMqA2Nf zDJVAyr`cCc*`E}s=;bj*C`hT8>7%-dB83?jFiaJKGoJA;jh)_7RXs2J#j;m)8Qd1Y zdC|xfW#=B3n+Rku3|ji^ByXepGSs%Vk?+#xa2pNZYf2y6bKd1vPo&c0aN7;lFJ&_1 ztpByZ%3=PTZ*@!^mTy&1Z~$TI0T9(eCp&9JjH#dBUMmMy21{pXdq9xW6h5Y_N zYLR+XNHdTMc|JkOj@{qpc}k9F6@)B{~+K%v(^FHAl5ddleud_3t{%r&#bAvsUqbR7^k(z4ql^vH;-$Z0M8I>)lZBWIg5B!QR#vq z8c39t9WAB@z~HdSl*Z>HF$H3zZD%u;?DE#XFHG9r<*gt5%p+<{^;#U9i~CX~u-vP`G4~Xiv&15}e_E@aQ8dwwN-*P{E49+3>)hHuPz-V=`ss*Oq zRJ}(0tHW_51FDd3|4GQc9J7s>7e*uselfsk%htL&vbj>dW;Y5UJf;p0o6T;;&gzXO zJ5l-9e8VCyADxfJMzbTd<-Z$8+YMs>l{b6o(wCTKkwzs1t2mRLxf89y7{^D85_a9J z^}|hUaY)Bair7lqjb4{k97w|~x{jrzNFas}W}D^a3Aj9m_DxHtM{m)J24CCkpxuzUW+@yOU5YU$`4gF_43MmKks z`t#Ow@<;phR3@{X-7!c5e*pHIn}Xq*yxrwGY+nbx(3SJ?sQAw>VYeJ!#4fDZnUT;R z@=_B3G&y5!2K+h3H}wHIVSOof{o!4spjzA9h$xZADJ4Aa>o$#S@6>mM;-a-wr_KSN z>dXrWTM%`m@`5B}>AUP?w`KK8j`dSa>hV6#3E3^*)93UKL^vvo6t~^o&NiI)v6PH0 zP^a~W-cr!*f6S)OWs-F)MrWSz-uC}&oZ9`4p9U(Y0z)3W=kSjoEi(R!uKppOJ<^6M zpEJY!^Ndf9$&iK8A1}gGDzSE6qx^;QWU8@tvgmnclMfyR8SkZU1peije@Y2aoQH@$ zL{sbpoWin;u!`|JR6qcJa;lOz{q1csXFTjTko`Wa3T%&{;MkV$T?d9?*SXQDl#b%6 z_T~3sOGl8Hx=PhJ8FA5xMqV+nVwU+{!$%vm#Y<$ehr?a$O=>4Br+-!LRpzgs_IHXs zEZLp$X146GGIZHzo{K(5>O_coGhz#RIB9g9HJIGgPPq`k{ zDOYG_rT7UsPoxvykJcw9ic;C_rn02UoC>>&b#a1`Q>p^w=41kA?M#y%frIMTc}dQW ziNnp7z7C`C0d>Zz?6dJ143w2ZkG;(J%G*)RYDPu}=NXmr&9y0DcjFzGVO=TIH5>`}BCmhV2uM@JsV?3QfNFpH+&k`m(kKlut zp*R(BrBEmA4vSY6VYkC8!1gMYt5Hb%Ey!tylyLET>s1r9&guB4uABJ#;v<~&@2ag% zvH71a>2WGOYU78zEU6cn9j7M|LvKBHSSyUaPAO?CG>vKs(L~DRQE5nb(+PVlX2+q6 zv4~TkAuk46hWL}tGk1!fnrq$uy*Pr+v<6~_CFJfz0%0ghLCr!Xj6!L|v`EXaZ$UG` z()~02KKUZECSN~ifFIvzKo3?Z1E&Ih`k)j4M9u?eC0X?!uHh?dI4@tgd+>Bd(NNsD zbsC4)6cPUtyRbYU&^{f}(*a7#tnPU3jzbs_`&BX@!AqJ#8=lqwLk&4dNNAvAIgL~K zMI^BKN8o}z+6 z`tiIlLVUZAdejU#r#;|_XHjPv;JI4hg<~(uoDwM$1oxyW&aV_}Ima%MjeEUU=I-uV zgHB3k=sa0GUmcG?h;Bt5$h&ZV1Yd-G2%&V|>7S&{$hIXA!S=-kao~DCWO!wh5yXfc zYf3cU9m}q_HbT<@k#uD|YcS;JYTz=T(>KohioTILockvzQQv0M<=w!o6d^rst4_Fi z)o(&w-#4lKrE+1T7^KkY&Ha?$3W#Kt+fi5^AgU2dJc|*&c1-sdlgP`5QD~mhk|9C_ za=hk%?Y;gfp=R;Xjgq6llPp16yV@Ej+O=6B zYSl{LL#ri4{AX3ot7F@zy{;enKD4n_wwLn6TT{6j$d?DM(Hs;@8Y_ILfzbRNw_AZt zAI8K?L2CzJ=?(5x60)^jb?(QBEA&2fp0;k;AXrGo@=$~_Uva;pmHINr@6kew{%vIT zH&L6+3{i3bD3JvpyTHby;iM{$3aRb4&`$^&Y=3=%B<9J{WbcV)ToFV5AlNH*t>xgM z@PePFDu)dBoz1~bm_!^MhBV>XP#h-B$7+{(V^1M;U;HzKQePQru*1B2G~PW{usbfd zOTKQMqZD6-vGpuW3ofvRMu^-Cl?Pz~Q3t%$D}#J^N|-?ENc}AwVSr02IB(|!g5sX& z4BWJnApDuWJWanrJtQ3)hYVimuHbHZR+-@OiJdbKcSzw}HjJxavP}6np7yc!1=s&R z5mq5P9Dh1H2-VFNoGP&hp;gBWiilOzD)IMWc(}p$0FR}^uMP-j8^!D7AozOhjF9|T zfk-hgHhZbTLlsceJgwKYwe7X(wboE;pn+l?cftHHt?5K)>WBZ?GWWqff4ZmEW3f&d zIcF0z7@efd6VFgUR|{cB%!bXPcAn}Q^9a(m@mg;J|0QNwP(~IQfaifsh}3W)m@o6y z5KX7@(I?(x@BL2S_65YE+26)|`ix>qNPmYdWSD7tjuo++^M|zL_`DaHyeFy#&r-+w zM}432VI@lSknhZ-uIQY+MWzOHnS31;{8ht1)-0@}>Cn;v;ISw%5A6nrVM14aLoi*H zwd?Hk*^fnz_NxB;&^nf7LxqqzU9DD4RV`%JvP&6!?BV&9$72M+Sxsa5$J&g`pipG#~lf=0QQ-m=5oGTWG)uge~<C7DY(b8yf|3i$bmAMiU=EqYS^z2x zf*};lzK9BtUW2NkhPco)hQY{(E1vo|rsLo~rW%rR@CqgvNu1!L414%90*06$;RL| zqXh&O--gZ$K?)!N0~jTj<2CcI-^(l(RTEY8%BboYYerP4F%_obI>HqV*!b2hP(Fev zc^uTunPVHT&o=D8ve@|O{cze%rFD-$cmFEj&#)x-P*%2-lZ%SI3@w6Z;v-aN2TegF zk_7sS1e$D}*6YTXs9Nr2^v7GPqep8pG1&js`&|89JhU4;Bn&yF&zR@g;Wnk+Y)$d1 zLhDB@tI~-Cjt3H4_!HLr>GGGLq@bmsQoh9>S}ioug7uWziIfa|EY~g9wFGVQB@sD& zD%2Hpw#nR0=ILzT-?oZIM+GTNcCk*L)0bXchSgdjlk3_P46I$nE3l@&^L}esA?ogq zw#?oYKmKuLc@{WyEwqD|$y&`RUY#Y3T$`O`w!{atbxfU*Oo2 zl6*#>w;@9vW%SpR(RUa19`ezL`q)>Q^b9s4AcsqKd% z$$jm<27@8u(QWgE5RrA@E9JVGMz(_A}oWj&Qk*=hv?nkbLd*Aa-7lXgih)MKP7uH9)iS&BB zgy=W^Ktp$s(L!8gBoL;*daw~}foIplPqq8E;doOR&-m9oq7&o}gTA&KlIpauspxgN zrXuIsL~r|+$2DM(&sVw0Jj*WU_sugs4;T!{Gpd-~JEudjw|w!!cG;1|B9&EP|OQjuLp-n?JFKCU6YH_mp1`baYJD%cSm~5YqQ@S>P zKI;;2ovLuC!(I`7Uk3wwtUw&{OAja(8;DqgM_A1pj*c3vjQwJX%0I43CE{>Cyz-$j zGJa&Gs?ZkcF|hUv75ZhXAsg!B~FLHIk-DOJc9K1ch#Gc&GwWG%wWv*K|J z%T#>Ub*n=chCdvdYM{y-h$l`~)mi^|V=GPGd$UJG)x^IbL8S~Ihz5V^!N=NULT-#a zpn3!yAyti%G#$tY746Ia8y#sYvx;b+RKNE+66ihqYyRmKUc5(q@b!j{GQO#xchvg- z1oRFI@o%Z`1kD2p;vpI8Lu0{Rl7iH+=65!7=$c7+NgNE84lOx}6`BmQsCayu7G}iK zg{9%4RyZECdSOIEM;Q%T4AmjEfux}Q;X0Y2>M==~R0l@-xFal@VL2KZ@;Y2Ckn1GY z?>_R_j?xPL0?%ufn^QOI{Htn_5G&nRl$ zVPww{C$IICL-xzUO9#O3l9B^^>B4Q(Sk+4hL=qbjS;uhJMFeoxA#1k9(fDtB=>q!b zN&t9WDI6S-(3kY1p=`p-`j&&WS(jtwu(j4fwn2TS7CEm^vI{e&)aJZ<7afI}DT#}iTma6$r1)?A56 zCXZsMza2f?mOEkcPWtq+0(Y@LjtU3%$7S{BoQo9-yF`p_aYUe^KJ{rsNMbM0y5MJNIC^oC2@rc

2Y_o* zL=~pVaC;0h1zXB~N`qofJ9AUuf~Mc~F6*feh!N4Uuf?v5~n z?_#+v%reO~j@G84AUfgHl<=lvGEUMbPb*$AYxSWmrN`FJ+0R1C@1Hs;_lC4w=a4}| z2J!ZDb6T&OzIgHU)%kg|Cofu{7xe4bxJ3&#y<4|lz@d}(GCwONQ#9OsEyCcYiKvnn z8(uYQf!7T(W7Nv|InE334(0K@IhP8B`t@v1S-;Ldf}Iy%^0Bu5QP+H@M0X~}jW!Yy z;ACoMA9d=x^j2+c=@ItPclD-@${N@%WlHmWmX@)0?V@AHcIUtQ-l4<}`x^Et?vgD| zk~i|(xTf68R5%O3%fM-;D510uwR?B7sLzbFeujH`%>2Y?tmy{g5!YMf|u0HkaGEAy*#?b_G+$2|Vp24Y3jI&$hcqwM(8cUYL)6m9 z3tb9NvkX&Wqzw~dV6Ze5-ep-<{buXh&8&#+fKa;D^&u@LL=D!{1w$gD8qw8@*^^%U zWv~FMxme%C=sCR2a=~f02bjI8jxJ`m1VGoW0lf*<1whxep~DDm^sk)dqKfwm{y37- z!qCvWfuz`J%oJ0~4gg;6hc1RZLfd2g90uBZ4$-Il(3`v&0b~c6=rf2uK^nF&H1zfn z8X8O}&V7jMjt4Tww{c9_((5~%uhL*Y_tKyQK<0?2mT z&>jn23U9KEYUorOI)u>1tb9$Yj#!ka&0KTc{7G%v@7m!cr8>Hpokg@^$#d3#*#yS| zNQ|(dLkMl?^*HH896ltajkQ8aiY*(C^!^$jUpLf*L z1#=0aZZ!9K2ioVvw?P_qqgD%+QXkM+m`JVK5N1EI6DB4(0EYV6g!*aoET#HmsTbXh zUKEHubR2-4P%dEYE#_{X@4aB#aW22zya9mUW}zKW+*n|3Ru5}0??dcv3oG_=YIVuh zUfzS~UCL7in)Y%V?*(j;GTv!+`Wv&;k7fsr=n1AylzmQPFu+_hD9gN8$JT%;W_>M8 zd;koMWj7j2o69cIAFJpVwzR+3l-9t8c#{w`+T%qnWyMk(!%SUw0kvX_(xhD)0A0*_ z655_S>kNP<{X18e9N~{8?S>iJjTW_(k4X{=&Gw>=QDVNk0%7RNLgUKjqSk!Z40k{+ z5M_D~7L^@TCjStX%cyL={Q}F8RHnB}P&taq=35r998G0<*8!DdsJsZv=2B000000C?JCU}Rum&-us0z`)t{ z%iy0WXFE^?1u(Jz0DOD}1$f$Q(*w+$O%#UVd(WKN+O}<5pIO_s&EK|dn+FW|0YjYc@>b{|b_NQ6o z92Hz~#!DBTlIyXvwtj&5*(8~^rimG4hMHD)=DSNA^Q0KfG(U~?176Yc%yd_&m8BZ# zelS^l37+#U>FyiwoIYa)TYn1u{m(on-S|*vQq>o<@oqeqMQrXT^OCKv7HYbN{d$x{ zJ!|U{nCe@QB^slGFGOKIgXnfrr5Q8bK(IQOdzzZ=JtFcW4K<05+*8QDVf$Ti|z8z#C$Qd~3QWFIg4qr50jF;+&?(Y;G)xk@9; zMK7O=sXCeWbq@vgAf>X5yA2>~Bl^k}nwXbOvZ-qtnM9Lh8r&I=m#ZY3MkdiDnFe>p z;%{frw5F-s#PE>$kYV2`b}VG?i7bmDhYyl&v4tUvR{w4OyqCd8S^gdr@)t7pL^W^c z?Lks6_DlHoR)1+ezn8T~Swr$(C-5cGmL?Wq@bF^feG{1CsUUnD;+zo$?sf{Lz+1&SZa0m_3aMm1S=P+dm7NqtCtN&QLV*W}YI)K=C`&{=dX zbt`r6^t^tq{;vL~K``_%%rv|(rj0|4=S;k5l88g-nh4w zcfR+Y_p1;2y7-p(75o()-NSJ&P~%34Lb(nc!Ek{FN5e&GJPem;5gRV&xI&wc;i~el;U;^2%n6Bj z6XW4>C&gkwkG^EFB$Qly9BnHCE_6jxe92;)wGy=9kjODl%`06cA!j+2XR@y}pIKRY zKs~iR?E(5h?LI?BOfVmb^W9F?)qN>qj4i#8)IJf(7w$OPLSkHOd^}|i0s2(7S+io! zf(<*(4!TV551$S-RCwBBU}gY=|5*$v3|IgFDjxyS0C?Ix&4C8OKpe;MyQ->|TA5r? zn-XcUf?+UHAQS+_0tl3<0;xBc-k>*V4)zr3QF?%|{y!3YeDCi6cR}6^n8_qpVkQgu zprcm`EMtbTk<&BEsQCXbaXT%X7AkpMPV71=)z(ExL8XUww^2rA9IGjcsI1-lRc=0U zx>c0&dEU2UDkk$vntnIN=am@Aeu3{~pb|5c@8uP2RDO`IMt2^#0868XDOdv^JU|3= o4I97%7f=+mwiY;|-ar>L>;WIVKm=UOSv70`%OG6h3*a(Q$d8jla{vGU literal 0 HcmV?d00001 diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..01d05fa509b7f91526cabe90c9bafa130e4c118a GIT binary patch literal 15828 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] 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 GP1dP2ZbstT&cf$d=w*b> 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 literal 0 HcmV?d00001 diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..86b386372664a278c839b4a6fedbf6a05b396b70 GIT binary patch literal 20332 zcmYhAb8sim*S0^|*tTsa8{4*RV`JO4ZD(WKwr$(mJo*0Kf8MUTYvx>apYG}Dn(8w% zu5zNH03g6mld}dO{dbIZ`-%TY{)_(qCMGN*3IG5l{V=f~8H2`y_KL~NDgCfb0049k z0Dw810e&VFQ&JHG0Kj{HcsKy?Yx{HVVO>s%o(TYel>W(+|B=2^22h!ije-3S%lh$Q z{3s9Nl#bZQ#fbm_fNK2lDE}wwGVp9udovpV02<=Q!}_C6*H`^YGXuw;Tp0cz56u4r z0YEmhb~pVAf5zaN1psXP$iCeQ&5do`0042EAJ0!8KnUdY*!1Qm2F5=u>WBUQPoI|a z9_ByrhYkM31V18yY6mMaw{iL}_wZ*9Kl1=VfC1HUwzf0+@k#xR{p-i}3n~KMhQ`Lg z?I)J`(f@J@fLVYcZ4GQpepuWOoBA2o2}^|o!QRf%2>_7Q|6$1gdB1G{#`X>-KY6l0 z9PskvlPEBKA9GuW?PCN2lE(n+{n!SR7lh4J0pYO*@WaFjQ5Js7vIzBRFR(JAGDFu@ z)z#Eh|0(-Z^T!!o4u5Jz&Qb{9NZ|Y)9vKl@5#Y8SodRmDeS#!91Im20jxC!$_JZai_GdIB+wZRr%ZDm~96S==zD7U3FG;xcW9Zr4`zAz&Jf|VJQUu!(i z%^2>T@!fAWFyBgEy7z3Nygsb9zhy;{fYaXAcWvz4Zn(z4f3cB)XkhFJ^(yLV;VKUs z9n|&F_VLWUpO%+#aH1@R78_7m!ij~Xh9VEV-)8TX7Ea%G?Crm<-^$%JDG+u4Mw?qK zA7+l1kXAriD9carz}qFnGv#bZ8{j@}h*P^GMr%kisWRfhUL7U--4I-DZN$a3 zI>yXB;mAEh_q-sEY{AqxCkvkKG&e;l>lo6IMXcX>MYLQ^xusQ;8CtIgro1|&9RD{z zX@c4sEt#hjQ!b+83h<%{y;Lu{Ja*<#l4T+@*<^aG5fXflok2VI@5rH8yVmjdXtYD( zCQZvIFy})aHwpX%@qL-~>_5>pk_9H>4U^EzB%OLKyoKzMBS?3}ocXO%lU^^+#Sb}% zWPQrqI+a6)?HrQ=R)#Unbi+xxaeH$kf>y&uUjYgIpf}`bMrSv+eQq z8tv{;wS=T0DQ(HiRm9w2QF$pBiqsR)WF)_yMIFi;UC2ha9AY=AU&ahPQu4&#HIk^e zVE%Xg>5695Q!so)eTHqybcwJTtm9xux24@e@aq<*thjkPa=I#4qFC^R&EA=B87&tF|1Tk`b#j?;gfnSVfJm|`eB+9}c8;WId7agcY?s+XQ!+n6%^_+1 zfP#m|PW8E?v1d)mGxRGab_1?jw|Zuy@V%3#AIdYsZ|E8PN%IQs^Bd`yTO5XC<3TaX zl-;`$hn~w4b~oABuO6+lYA$DJBI63Q)4H7XrG|?zwi38`f%8(S8I59iqMPQM);Ikr zre-^q^R6ELqbrb3Yx8Eu#!YS7PcalFknV!h?tTYoCHJ&P>C6T8@4r>|#A|EnPnh}~ zWwJVaV~&n=fh5*71J6FJFXr$oEd3+m?eAK)_2TZ0<@KHFCDp{D&EM;Y7eyfa7dM!^~xqEy{rfQbIH<5OgZUU*T>d0$9m()SwBpcl$RonHv|dNd={tB|Y>%R0D)=w#fO;s;-%51rOh zJ6IqHnUe_d+7OlKK#Kl*F#s}4I{3SdlSm=<6eJ#xBE&P5RGNOl5u!~lC}*Emisn!W zfk`gO(;+xh)7yZ(zoz2mVv?g1ZG>OWQqvMl% zdC${<#7i#NO()n*_{*mk9`oFS?0;Q9vM5o=JdF&>%Q3Gk!0?RBI?~+YL~@UCV~rxX z1;mqMI7THNy2ZTZ7~PBPkJ07*Y#MIsI`JYdTl@$jc92t(+>i&{lapBzX?*DhNw6Sl z2rSGET_iKn)mW#aXr1SdPv^-+qJA)@M=aBCkt;V!cNJZPqC*`aY`JA&UFMSZDB${z z$nXf-Ui0E74FA$g(a*!OJ3MzzhdRv8**y=IpE&V$xF2Rux3t)zwtGCMxR-qJ5waKv z^)1G&wL2l6gxV7|2q)aK#fLfGn)imNr$=}@J^N&qJ}PF6!jIW+jKgv~ZM*>G8~ajM(lDt9XiDs-(?LSZCVXJpnfj76Y~TQ*a`Kv?u{5qNy}gDx9Y zp<{8{>)OU)IqRZA@G6LM@SZkHpeM=*v|-8B7vo5OPgK=SgJu4bA~IAEKuzI@ECWMR zCo%F&t(T0T%uiw(Zz{|n3Zl`W7f}k7t|NxzU5GcWEUKCDQl5UW~aqPVFSlI$-i_;VT z-&&9k)d2}J006yGMBheU?fCqq^OPvf455}YS?FOffnH$GqD4S2~{C!(Kyw`DYgN0WBk6ys$pQ`KI=ysak?4Gn%fHr9e(KtnE z&hPeg1H|HwqKCzm(~U=?cgVR3kv1CF!+7q*)O*$LQP)2y1eBh4_ea&<#Vo~u)(ClF z#v|ofelDKL`a=TuEP|~R@cYBKM~&`mqO--2`UgSQKWePMz~r$g?4G>8W0&()ERv74 z9oq*WkJTNcIOcIoZP`9Dzb0==*-DX1pqz`9^A1|Ofv5QaW-8Ft|J!i~7o_=sVVDLP zh)Lo`DU70+`$!~6n1@(QY1HfD1*slr{*EF|RKTn^skJFdqOQXk21(tgj5Z z<%_x6`F#n{hOi#Bwn3l$J~&>;6&*dRppLU&-rPF%Jpyb^si*nMk9Y9h_$u4`zOsQ< z6iXsOKKt~=0^ySd@{TYB?7WPK6J*;f(-V!}KG}s*EMV?>c?TA;sWs|BSCzNW+J zVEd!VtXX0@cZ(5O*7D|3`)YE;K~B=UnkkU{*$MNLr0d3MRKw+k!91@-bL}D*uGYPy zyYdChtVU@KfkdZmw2I`}$+d-T^uDfG&AtUbu?bF#S?TMK6@?|O`HP5-&=n15ml8Ki zdq@3>m`n;jE4!+5G)XPPFV~VFxVO<K zh@b6E3hi_IjPCOQUAv(xsz_Z6s8jU@g;wX7>Qi-p!%B1)FUrc5wH+E$XF4`R+w1yE zpR}n1^Fyw$Y&JAf6bM3w1Qi59AY=gn7}-YwO2gW`LbRgt?StyFX5zBwhd^-YMg`N?y`1uk@rFb#2v zf_*j+E?;Apu%0jZpSY{Rq70FI1PMaiBA1_v+!Fs4DD&}TVkh4@<2C*Ai zbHXO-;OvUeoQVIm2C?xIAoFt%-2V1+Q%4@e9wHE3+ErQQHwcU)NjVqr{#`a>VT&>} zH=~M8y7ClYFGr>FSQ-8L=kE9X49N@DKzFqABB#%`=4XY1f>G1R{wEcMrH)MrV`^{T zEEf-4W#Cr_=)aq0jwavIUQ-Fb?ep%64>@y>3`OfjM-@4iXYH+r*A=5L?i!OBti6N9Sy>(hy*`3ce+1>I6_akZ|HR)J#(xHTe%tNcqpFh9NBomkw znumtNtr4iPt!3dq)^V{JDmqYWJPQa%}%Cy-L!(H*c3aj z1ch=(%Fjy3*HX#Y-%1+~o3E-U%demR{3ferW*jqG5q@%0Ou&FmOUTIcPDJfxd_J zGW;vJ?E_pICsK|_g<(SPchyCH>a;=x`tnp!CN3y8QmKq*#nnhL%RVY0;ovlin9LtUcB7}X($Od)s^@$!DC!0-~cRRdm%~vHd_jfGs@a`>>dzMeoL}Xbb&fnL(ZP2tO z#6bCkm}HOf%4NWieTnF6ZxQQb9~X6h71dblwv)6|ZI)PM47=OQb?UJ|`hs=N&VH{B zHxmBJY{@5wo8Erf!`bWa?UY!}?PetW7I~l0m(J7c8IA7&SPh}v8HELb^`a>WIFc}h zXrv#%S;I9fOn&C9$u#TJ;Wk9Z^lj=0KEh=LjJWC}>Qy4r4xqID5-popxsS-t>t6wt zZIp6ks&jC8*S{-7Y^-uzt`A@>jQJ&~gd|rmHbUSBy1M4Ba8j_kOp>uYWGC*0w`0W` zX}z$Ea?oKAhYM^p(kp}+-|v$;e2^Lq{&xwN8PAKM@ZfKAEYEWD6mje=D~*aqK!J!D zHG|XMUi{bMwQ!3oHD%w~pI8|5SxaT=>Aa|s^IHD(`Pj6QrQ4tmQF#E_L}SL%Ft z0fHG3B6q~l*0!Ou#j^o}RSc<26h-ChbG!QUz>7)$ba16A9RbIL0~(|;_m<2{A^4W> z-odJWPCj^HPmg_flx`ITpC>Xx1kfny8FJj7KLJy?fBdza0RMgnA(>||0~h(PT$A#U z1seB^_OW6eC(Jq)B)KCLyk9{AfP&01n^4<4=>GPmuIJm;gBQq%Y`bC#U8PlYs8}`L zOdUwu(_8)MPq!zdwD{(S=I1f>4)?cs zt&>jlQ&3I-0+^Z#LPA1aWuB7YOg@9b$e>~og><0O#*p3&Vqidkj- zX4^^y@O-WPA*!%!2lE+p-z;ts>N@;qP99^e&kN?`DotW{w#b%o^!c5clm)%JJAQKQ zND)PSMbVv7-p=>4$~LYyN^W?9-zKF5fm(Ji`NVs#q#|aw@;0|&ve#-uCH^h{1i+M4 zA<&DbU3rtwj{^aT7;Mfgm9G2~8O+Ufpf=V|aqE9%9Mi3{;Lsar(BbB@tK@fvnrMcL zasNt~F;~dkk)1<8XGk-ELQ64$iS+!-D{r(-|ZRcn(j>v209 z%F3=e5HQqNNVXPBtb^zV$!jPRmnOqjY0LJF%Y=|)5SZsyd8jOLd9ctof+sBQa*g-WJGDX9r|Ahom}Jb+`T45wvx%e^1uP02J*rnUK?x*wfw zH%z2fecAs0it@>n{FuX>C%e1g!tCns@p60#QIo2yAjlLMzS~!d14-cnf~6V%B9#Ur zon%`+*^A~`%Nn}iy_7)rJkDSMHIXj>B>m;q_C!M99N;x5Sw%GtgI-{MrgePM%n43_ z>s;;c^~q{NGrK-|v@bkYKx(qo&vG%4vz1*Z;hI{4SL7-E+ zM$Fm$7RvwzU!y^O;$kqSKwEyIcyO_qD7v)2pa0^i~?+cLUeFrW22`6Eh`2L3=JvV z(tM%#$4P^SoH#y@C;8j{!OV0gZ?}|%vqcx9qs7gT&~T|)jOX2x&`2j&#K_ZV>4#v` zGsb(Km+m+)JU7q*zYYGRW)w_=LS^D-ciOXP3?tbZIi}GodEx$*c>m;dZL?Y@tRQn| zr_G+47KvxJK_X+s3uA$j-tnMz23%>eW~;J`z%fT8C{8#L<0x8u?4>3me-#=u^hp7z z32m)JgTwE`LIrfb^(zUTW#=PJtI4RYGalWu977#8_crTduJRiQo=*egMN6k_tk*dq z8GMd-pU`7p1XPuf2H*B*;+>ukt8Bb4{EStQ4JWtDBiPrI+Q-a(-mY$rJ2a>ZEnY14 z@24mDKZwWp1ycVyMOKoV9c2GB(U1JH_EY^lEWL0d(CK3GeMCV1w{$8tZ7c*Rk1q-U zBFbx_Ct`|lM9nWN(95;m@m(Qk@So%fP$0qT<5YPTF>mO*_~lfX_|zZXo01h+q(W%n zO>oQo&k5RWA^38>gsWUGg5Qq_@FzUwVnl%ZaYano8_o=zHfTMui_yRn%gC*L2sjz3 zBu#w+v~4Oz-js+d9#&(1-~1dxR8b63_iL}ToVfI8*6x(hi~WPuBQQmOf3x$b@>E7* zOOMIJQEFsb?OBgp#D6DsN_b$n?VuzUJV+XimQL~mvo!k=PdOFy;UgJ?&ov6IYon?Z zyZ$kvGV@d#&_Eu1fw-D2A?*)Q*-xvP;Z+Aj`yH?7VGJvJ`3iQiE_H!==;LvoXScxx zdw00v$_T8Kia(IlV!k`^!Zw$CQ}gQY({LuN{lhsj%Kr?OaWJ4tq znH}shN062-WS07CP$j7Ls*gDxhH1<%~@Finsi_&{wdC9|iL5#UUzpvzuhV~{bZ$-RvwprCYV zxf&*(@X$Zsu{ocqoZ~C(7=hU2zO5~HC(&bh@J3i857%?UTk0n^C^41gG!CVQ_Y}Rd zQ`aOh!bHj!NbpSiOEr)5OJdab5bccDWWz(LMf3IZLT=tStt&iE;0n(%67;=EMh}0X zW8R**IZx>usUF}xs%^wB8!1N%jTfiyNxwACp+vGH&SW(}bsZQ2y!q1YXG3!{Ytl!J zcc_qS6{S0M84W~V{jH9$N;jr7Ta0O7Nm7#H>J?qug-BTcCK%r?dcU!WX?*zIvOF_7s$oDF>66g}N6W)>^sJ<#dpnnt-2usNllN);uln zJmt-bj3uz7M+(pZ0fFW~{=?i>bt+7K@rO%ysIoK4;-`#PVsJJK)BQCC zBi~B@>Y=}F==LOi_1l^YMD0#uzQ(&d_FLy4y7QV?eNOsNhtMsrhJiA3qjGukJe z_Y=-~G~%5t|M<$6cUNC8N%jWEl|TL{*`>w7BCB`C?-pMB*+lIxI08U6j~{#^+r>v1 z$bRRm3I$8ss;9@1VNEJ#FbY0jOHDPe_aabkJs-#0lLZ5IF85*mbTC;sd}d~6KJaqyI*#y0h8iDnhzhbDy+U+SiMt2X}@((H9m} zE_g}Pk}2P-XcUTFg=E7TE+bq1`)+?-B_aH^+N&eRjUr_dpbF59{1IreS6^@-W{PC+2r&`UiCs$9i)!=BE8^_ZSr23UJ zm8qriCi-*5Q9`zK=3$36*TZYLzmo$Qd=iW2Vk&&wQ3-BFtJUE7Efpvq*DtN~Xl%^G z|51#9`hBq%|4QR^$4ULZjKfIfc17VyyzrZIxWZD1FrYoc0auw@2kvbDpK1v!%Q+1mAgM z?EE(bPSY&L1?|@PqSZ#L1fmZwf*>zd4u2z;Imvo>V7IF39B}A>!2|VJtYaW+jk-+l z*d))EaUZL;WMeP$HY9J=$2|M1B@5g2THQ-KR-N_-JJcFS7ZLfn%ZK*sJfH;K9^p8+ zCBfei+^bkkgy=e9@dHQu?VgllItAzwT*s{?0qI6;V+eyapn6v`GHCmG*esy*n?=|b z|Ippy+b4oSi)8X-2;!X}aL7y!Sx#6&2)CxnvATWq0Dnr*z;pe-v%*ffBOx|-Zn0q~t(QqKSFITkZ*MnXuw78_ zdOTIkwciqNSB|GDhd{MdZ*f=V2S^lsKw)Mq)ZvKfKL%$p%{DTkXnfq9b2ABWX_z zB3%c!8X4OV%;6vmRodg=0l!#aw2r$`r1n>Y-jWZ3p4{EFLx(2ZG>&^5er*h)2eO}T zS28IFu&Lk=$GHi*y^pl?bSBSRU7AVciOfOmaovHM!%XQn&un)FtC~_<3OXOwF(W*` zH#te^Wg>T^M2g(UhLlBSH>!=~xqXS`E2XZIx4FShb#;7fPat05joHRDZ8Uxe zn*Hjbu`Usw{MO%q9JJ%h;&c~&uAu}kx#FcJn;Z4X%(6P|P(t=F^=+&&cg;0W5_LI` zU-5m(Y@DwAN=60dO2s{A!|^lMWfuS|1AfwPoKfrn3N=$u?t`D?3%;Lv=loXtz!GMH z(uBeq;$l7TzTd8qqj=>>@ITouheT_T>gZ#0oS`M#q~Ty3Q|@#`qNCku`+bsl?aiy* zn3HBV$82>xim=)el~W3I){r>VsB$O4|BB632jyZIy{nNs&z@t0=aQ|~quO1_Oht?y zzq2uPuL>Xca8WbBAF!q*)4?pk1uDwnZanC50B5o85BXi>TB@u0eMdixJFId>t-F!N z>b;xSAAc$swsU`yFmY2#&#St>>7CTT9klnU0T$-}9(Ckf=YR`Pz)|>^y1d54jp4ec zpi+??3R6UdRxeyNiVicRUzIOJ=*0t7sQVQGYP7gI(=<4+4NF!x%pxNveWTL$l$*|T zc--4lx_OP);i3-w602UB0bi_2 z7a8-wo=Qrcl#b-woK5tf{qv;Lh3?9kn(UW3t(N5(pT4gEX3QtL2z~#bSbZ0uhz~K( z{bSX~i{XvNr?aA`oYl-kVkFnNa18;4b#j$1r*t){r&wUKSsv_u$e{xyTv@Z^n{jw5{`VF0R325%L? zyd~;3h049n-D`EKsqtfZi7`?yf6GbuJn3+bR0?a3?%~#=y;ux)52vEPRhcAKxR@2r zs8e9ovK7@XJz4(l38xdratzc_VuPgIBhi}(JcDAk>AhJD%n?>Ua}r~^sn~ApDJazK zhY0?2JUjIoIm;wY?fS^CWZ@=Up5&ji;6dlpGPx)@f+-1vM7$UInztI)Gj4wF@PJJ6 zN?IW&8ew?YxRkEL-LSf${xd(bLRLDM=sT%jW}PKan_*4n-;O$ek0XYw(P|`=WroMs zR4*_VznMv4w>|)0>4h|N{7<88=2i34%{U0wVElnByFs48i;np(6P%ON)4mJO~L^>x+=C1Jdp5( z;7KG?)$jYkLBNi)>e0>)|AKzKjE9bJX$=)!qJn=IQ%qPZv|2t_ZH6~vc)f+*pWh6U zlxZjx>2YFEbbOq@`P}L3wLEj;L9p(WtJHBpiq>qJRS$Y)sDdN6(_{U5U}f(LHl7eYOmKk zLY@|N!YAd9hNpKg86@#Kqm{p}r`E)kcgxvtMHLHYX7r}(um4PWJna8`mj#SG=|{Q{9;UH=gn<|zbCx4%L3eqd%wdV zh(t-CF#_>+ecyIQH@H~Y`B?2dLCqBkA-IK!W!3%5IrD-eFZ|ch^w? za-cX5F6X9;)@81|K7P6w3AC<8nHvmJBTxo5_Lsc7=Z%?L5~+Q>o12IPZnB>B!&V-|*4p zus`{(yAZ;=(QTYnKL1rjs6b%-ja&FRRKtUoTLEpkENlhKgTdh~ysr&IrWeLQxP03kQt}EsA5j zU+UsMULs423V?ZBhjSpz`opuMPaoasH`H6}7nn`Z=Cd+zHdjwB3`60}f|Zeoa<=O6 zBc!7pa`^&njyv0@5OTqd5w)P!^OnJ)b5B8L{#g|MG9nwtP$2C>9EO74ZyaI=WK6#y zIOo3vJX@=v{)PglH+tJ@%?0Hzcsw5PGAhm61EKQqS6UmujRIqXd#fN2QIc|ff|>_1 zm%_JmOMHrxbcZEF{2O~BDu9t)K)d#m=&pSO$F?vBbP*w+ik^Z;YzO`}tLb3T-;(i8 zszX{~9ynDWWNxWJ-Gh>F+S4jnP=uH;3E4D--EU~qq#(x%*L{KJLP2c{3x9;ae;5a} zlA7EqOL$&|qN?B_jJsZ<$f)a6GReTLATgzg*;_TRo?Sw4Mn-~1Mg~X7HwVy|#hk8H7+#8Ye+lR%? zw{s_QMV9F-nGAp(u}xvF>+)FKNPBvcHg%!k-NmLjUr!D%#Xp~mdwS!pQ@@?7d&)ot z31*A0UkY)<5kt0(ya&(m=vMaB@VH#69vWb7b$MRS9>k1Z{TuM7_wvLAb;&_}NPl{s zvGS?qd~$~&P>))U;Da{M51^}$8A8chZoD&lO&ZF~2Lg-1mg;s|HosPuEu*saFSlG~ z60u=H3=c>5<<(gA?#o#;^#-%$Q&_YLCM`CojSsk3hTmTiGmhy+y(zMG9cU2y6+%=D z6OHc4JVUhH*O2GJ8*_~IJ2xZntRP=KhgVyU*6m#3()-K86mI+b6}((6n$KT6C75e( zSX7W=E(ZZk_Cv=i8;o;cYCRK2e1Di930P(SaUs_S53T|@*?bWMQQCMsl{C)YP2yZV zAl1C4su{0M)p=FZLAJ1;^eEC%H|xc8w`C_dAM9n|rXVCg*VnLJOA$DOKpYsM{00^3 z;RtM+))fwNv=8j~1ypQ;IB-t7jUB^e%q&oWa|J&JRKingK4CH04$?Fz@SQ7M8qB8b zcaq{0(!rGR4pM7ZI)c)idn(*3kjJOo4H@IAiCOSP?#i^6m&?CLiAZe)rW_YvrS%lr znn-4CJZpjdrQB=0>B)_xT4g&R>Ejb>N|{g8fILj6q(91z&aw4rcE5U3-!_YM8IJvg zBDB-9*_a!avoIu5B01tQe}|LNn&hT8j_R`vGt{#h8HRheY_-?foD~#=GcVI>wmj_@ z+hC_8$ztnu7I7La7thd}nCRrhJ6kMAkw!wLRBviOO~=d!BwS|I_sekA=&d)m15S>& zy*XZFuP0m`>Tj0w?h%EtF$kr=5`DMCp>CksiP4Bf7ojVPfhE%lf!jH?6Da5FDHGCI zpkOow7MD~MybI{REEQ2!dnnMr^IKh$S;ZA)^XbORAI3o!{Xc6}mOJS|b``ZIKC|7& zF&8QA%y_#Y{S0vD%)8P>7XtW}EVTbhrNkkKpyAaamMyi`)Ym6m{wg`Q3$}Wt?`4pl z3+E8lJ;;Nmi67{2A=W9xl(>VDsFpD;sAX#Z+eutFvu7-Z)y=pTQ{|30cDR?GvKO_m zGJj%UFwg>vtW~dw9I^3EBl(GOiqr8_@(-qF^0_jl8SD!^S9q z=t$M!qE^w9b!XaAFkYAm?NZ(D{HgtZCp$y@65AtC@bpxmvY|yU)aO~BV4Vwlu~uu= zPhBSPzzZBXo4*jKi5lhoWqI*i!|p(&nBJt5y}Y{HG@;g2=i~G0^IoH{ z_Mx#PSN_-8c*M;{(NA7d#qEqF2xZhazg`2vU(j? zwj5Ap7N03-O8wC7m(8=ix+|p!?FD&8LzFd7FJa6Z1af>u1M%n3zgkN+m$r7b$HjO( zj8aJyMy@3vsz{7-w8gOU{@`c754CBKEyytklH_q$=5RTxcerH7Yy9((eXu~|s=L8p zI(xrab$J9lLc3P$)o6uo+38CUwb7;2*3$w7+u@ zz{6#$Ay_pMBr+tGELHe^@*7e!IKfB^uqP24s%TJU4?<+HK@8gX7uMOm01)tIUSlr` z0Tlk2(Z3dV>7B0jGrNs6Wgr?NEW z;Qcqy>o;VK8T^NOft_YSp~bMd=cM~{Rx2qM#aoLi7HLHo=2d!z6iD7$0P8crqI zM-aRtTBn9E3w1N`Knm?Ryfn*PTk`1NKQjTT@wU;ayUY{yZ6C9|MyUtzSw%Ng$+KxNeX8L*CBs_jEWbb0ZXv} zZ_Av4Y(WUZ1zD^zn15fx8Th=%NlpvKuQdB}l@_a^_k=fMdwQ!IKzkB;F!Sf+20SZ$H2;v*s8Kd_Ua!tt`dE2ew zeL6nZ;C7b`KNO)IF1%KZ3tD&+=1oyj3oD_xaHW2j>)$Ffda}rGU}#mwjBZXTppE63 zI4yJTKXS`;N_h1Bd%6F&h500cI1n>j7>h9%?ASy(xz7MSl+UdWg(NUOqsw|!!3GUO zrkU04K)!RP8JsmYW4`9(LCb|WRVE#nwhSXxPQd@5quFw&a%%GFp;Tt_vI7jkskicu zY`$MyoiH>l^Op7ysV{2r9pY9Ixi3a8^i3W{ussWg;-ZxeM2H2#QV_$Y4vovcz3bQI z=ec+&tEoPwfrHRfG>|aspD1_dkt4_c%l=yy@=2T+ZSWEox$EVUlN4O{;LHx^^}j9p zV>Q*@T&@>&?q%%V-V-MYhM6(bQ|W1GXtRxO4*xnQPu80vr~3T})IJf|%EH}@knV;l zbU;mG=BIa^EG3~xe$o9UQ8>S8`A~i{3It1n*MLJ80P{sk>_vZ{$l(SL73g#J)&MKT z$l4{K8<^0!D3`669_{5t^H=VZW&y~gc{d3i*{(cxUA!a9uQpqc95WFf-X&{Hcu*)H z{rsf>5C;FbEHIvAf4j9l&yVUADaJex!zv(?DTAZOeQmmDbNIk=H$*?$?vTTKD!kDN zWmbrp1=j8(fsWc4<)%enzkdn5ZfMd>q-vLyK^?-M)t);&G=BfIE@oSYW>MUhNa^%` zt-E&)@QFQV-NPy{j~8_ww(=PaP|hM$Mg?xLj3+G!!NaDGXz~s%ih>J=*5sn1W~OU* zXYMb~kFwB?@iCo@h+#4eTWCPyj`j*5cagm{=88`taVyAe>5LtIO)&W~%mk$eBI_Z9 zHbrxWBI!a_4pPrUL#w?tZXpn8a)SGS@CwqiSM_)@owU;9Z7Tmq4*%4=jQi(P!(ZX(!h4?~pSkIWB^YR)u!0}do^RRpXb z0xZYdZ*tg*`xXd29{{=o48ZsCQ!hbA1Ui#>o^*Ky(q3(NvBz{b?U@bZ8MvETWnKU( z!Y@v|{3^c0j6?wD&*1>6ws)Um&Oc0-e}>UNEto{F57)baH{jpJNe(}gWX3W_X?Xs} zUlHmhhza+sSAwD;I5s^k^3VxVt%|#%9&PzG8$}ETF{ng2+$Fydg?#Pyyjb_9`niUk zdaWXTWjw>sUn`fxck-DwtYl_VY~y<|MGOvS@}v>Dj(qUic09;AEaw9}3uka?r#E_p z={r+WJHZKC#x9#(1rifzF=M>D2KbCQss!?M`OA-NJ{nSN_FX71z*Xc?+;Uaj!iR1O zVYljmZI8bJNzD6wiqMW0B}%zLB9%VhxcE#wJ`4}&i@c4w<;MZOp3PVSwKwTQAFp$D zG*K_3Xq^XkK)ynv-%4sK!0wO`@pyzFB{CKikDQI9P~l)if8yskrXrU=*FH*fh|+!% z@^(xfxD={8EciE02=KqnaE-ex!0!bioJAoUH*BD2W-3>a<|DSdFE{%%2YY9TV#&fw z3q&7uqk?r;H3#ut*b_IyVkul95v||EMDv})dP?XMCdN~z9!ig|OF`(eyDL+voRJ^C z?s`I2wWS_hni4x`z3YHr)V#H_`JkXZq2ziGz7IW_9Ry11$n1F-lQfH75f@Tt9>Kp&NkOCGjV6a*NnP~yw9Gu-qG9Z_*yS%0*$aJ8V!nbU{! z$u7SNGLI$+6=pnSuntnbCGNOvJ|bn~(M!~dg0^L?AUwtHUB;p~S?(t_DHuV&VqzKe zV37q|!pfg$H(@%JMLvlh%}qdi>xN)8a439nryrEa|8z~_@|PsOBhi)w z(U&+$4JbN3@FRa4+c^z{rf+HSg0z`15Ya6UQ8-R;5mdUG&?1F=}G4 zPs8t1`(4F>xjtgJBwG%43T9VLU+=p*AmDd6g&Z%g35RAidjVMq%l=rmb-(R1!OiZ1 zUD68x!(<`Hea7)DLP6M7mCe5_s5h;Q%^=N0mO&2becZgJ6T9=(bYjp@?x{%Rd(zV` zBf~n%1R_7>0*GhwG!+8XE$X0FUp)`sv-%GXW?P!g_yvcfP@ZX8_rj@}P}!HXFrmnh zT6gD{fY0cJ-1}H2LhnkOr+&HAnh%$`$w=yzSdFY@OD(~<{<@`}7TcYJ%WKqBUOoZm z&4<5wUb0mrW;yx+K=rC77gp(J1;ygoj)d@3&UkXwgwcgMhYyea>W(EIMyKUEC7vE+ zylS`IR`Cq7Xz8K)h~YU#N zV%x8g?zuHN?Ch*|mmA9p=Hj8(6M7>Dp%fp;T&P!MD4<#j4NrD!bK-z9A5f?_&@Bon%&M+dq}<|w zf*d8)5|eS*Fx7I*kQ*?LNSiabIj7L4YQ) z{NL}StSXVSOQ{k%Z!4lmnhNr|czt01DR+wjM@P9ssu9i>%e7+1$~4*e07CkyweS0h zz`_nEtgR+&29{mm{J=-pG=kuNAh5P=aoepR9s1A2Rj0VHG;R;wnSFZ+gZwgHUWqo9 z(F_}WoQH6B>$b|ufh7{|9A|sg%!Yx5>*6n^G^|dy(zeA@+6LnHSNAw~SuVRoe~J;n zw)9WHXD_Z$&YPu0>gtb z50K(k4xRP}DWbbSKAk}uM)ZI|Xhn!Ubb&WLaVR60{!+2UfR?djL?_M%v&~Y>t$NR$ zFGUW|ONt?mtCi8>5Z^#H_nh?57&SR4X6WMV_d@PO%7b0FMpsQLidQJ?N~^2u6xW+41nvkwI<_Q?|y@ zG~Ca8^z%}UQM`7d-!%8O8P7&>nd}6GNHIc&L#|oi(Q+};rXr)WZR()YgRy&i+_PXV zjTX1hr23@DuGoUb1lJ$>f7iI^;I;WJH zV_KGERav9B3R18EWZ$j%Aw6=srfHm4tiLd3)X?%kTla=G3{a!xjSl10(qdUC*k6}!FG}yB zfd)YGm5b7a@$~&HxWRg1RjXr#>5LCc>pcJLMqo7iuc8xCvIT3eV^|hFKGnR!YpwiZghgfKKBo?x?e=AJe;taMF_>(QQZ)yg?q4$9p1`G z#&tEg!D1mwo29bNT#_#1{T@|h37WZ$VIdL$e;U`uE7AIG`T$~E|RQ~JfAnK z2V=|wY5aUW!nu90-i=1a%RCf#jmTQvm)y!UX}zkc>(J9Y*T{K188c=}i9BR{i4bYp@Li!*Dm;SOJ+Aiq4iPV94n*cH4_*5ug6Sl`iviWMl%yM_2lf2WAfH??T=oU#|ZPx(-G0URGbU z>}3G-v)~6BNN@nW{GH1dh!UzBO0Z!1`+OLI1rY#~{P#cc79oUT0o`T6KY))SsP2Y; zJ;gG8Fi(-jbtn*Kl>%Wh(>w@2O4Y?%%2pO_M&(5-%kY-vuZTrUMAY@wgZnF1?Krr9 zi}u{nBNvX*$RKIcCeo%zn9WZDUmH>l3FgsE(c=qXHB^9*UMB(w+L{O;<|BZ0LVt3@ z$Pn`F|4RfAU4M225JLh0J}LOssRrr)pHzdy^;Ls(*Em;$^dsA@8O5Slofh}Y%-o_B zYEaI0WyvQx6t!s2p7B_t7*S{gpjb+*}>`l8$V{d*bDbt#V zgbf_jH?J2UpwP!owti#z^~*B`B{JF$Ee1(+toev{Dm!+ebfqoM#3yZC@w!DIKwmN% z+k_C%vmEGrhvFgr&YbcHjgEQw(Z@fD_&MT9gphvRJ0dtDnw-c_B2d;*dWB~mHaUhN zBTNi7HbtSOk#{T5Tc7aC-$vfgRZ(7R@cHP=2XMO_^JoKd-c~ zPlx25z2@~=k(iu3BRK_ko|D*i@#LJG$(=?8CC-^JXO5B-my{G2pOl1tiEr7Wb?f1Y zfT72efAI#=b6?W)bcemx-#G;ed%LJk%yF7X9*v2f@a8ob=46)cxB*XU>V`}R^qQAP1o3nDq!otmhGAprUN$!YdecQF|m5Ne2E}xfEv2%TUWvl_sAHDVY zb_4ni?A);r(C8*!iML261T7o{z-6F!s!r@fJsRfulo8M8fZg*ctIzXc_ob2d?Pm8K zX7~BFla@EW{<-F?jd%RE6KK)sKl??&k`(m>!NsM9__^{$#ar{t~%01&_v)+j>k z=VJ+@_cH$ygxmkuYOBI++nGw7Vrwjv+N-USRty2aOIiJ9zXP;Y_FC9j13HEgXaU48 zSxmMeO!{wCc$RO7C|!=GAKOHf#W@^AvjU%_FL7QbHM$zu4ME2`clvN0jU3WKcjWBq zxpNNH?wVG=`SIWA+k2l@<4vV_er?e^yh#y|@4xA^=$#Tw`o`)mY7=lIG-lECE~q6+ zfd8oLtij`mfha8byOo)l*$y)^LltIb=9s!L=OA3AP3tYgXllaglP27SaXmMyoh4m4 z_OJK7eW%kNX_Vt`@1v(Az~L7Px`E#%nljK+W6(j?rO-|nPP&TdFl*zZeA;ON2QB^( zy_Nj}?R<}RX5cH{q!{T(Xhew5<`aD*(!|%+0y(1T|CDGSMHo?!pQW_$=f)4iFgpdt zPlCVqG&5s=_$13IF3NTae!795QqXBJ=pehGpfiM%jwgC6djxd8ht9H7FkS<95cT_- z-i>RdPg_QG&2)n+(;Nrgz=je{KSa@mNgE%4p5~x~#sr0Sf-vb9ybOV2*psQlgLH0>S z3nyJebeO&4V|;YA)3^9X^j2OB+W8*39&#BVbHVOS_&ggaue3Kk&*;9R=b(2#-N0|y zG`@+C(z!9{AbX_H&KC|^{2_WP`wDcvhb{!~)zUk55JLU2(#;9dr+b{9EZtyN7{P#^ zDxY~UizH27oa?<3)pP@2ub_J?X~BX!6)bnK+Gk?JTLV6}kBe<17hBm?aFOq!2k@`0 zBQX=p$ub%dV&7P@Z$uwRahM#j{Qs0_?-dv^U9ME)aizj;A$UzQWnOKR0PS4V%P|2z z-N5H7=n4gmd975i+`(#Xq`Z{{?F@*ng$(-1b*de>O_~8CPNVvivxaLWiSn9sa*46H z;SRQ;ShDsfgSs$T;|Z{REEdWD=z)kul&{MQWL~k_~0-f)nlexV{;qHE%8FS5< zu_oa2k}FfeJ@=U4Jo)H=JJ;2c?W z9=4qmT>#E2rE}cXis~z*mtw|TYm@C<(x7kBf^A)Z>hy*s%I1T5g;ba4Sm$Fc&!T|30BbxG0_#GouRuLO(?Pu`Ct4qYx=MQl zbqUtkc^~Ui|m13?VKz|^*~x&ME-0gk0~5P+)cv(59Q zN|Kw?vcpceBp}lZx<#||-ZUsm%)%g{VHuN?#BvK7^?o=pwu$8AoRgtpSb8Q7`cxlD zUTw!TFjJ5J0g?K}(&~#JT3m^Ix9SUUqXeD+0000100002BNB?zCVXEHJoNw>2mk;8 z006}B5iS4#007kiQuO*8{nZID2y*}c00{sB00000004N}V_;-pV9)u-!@$7l`Ahnr z7pDPG1O+g%003`<1Z{ZQb<+c|T`?F0;7#)9+}gHn+qP}n_Ooi+wr$%T)Wh0#_t|IW zn>|)GW-59hWKq9}bYiU3GvF(4Fj^*IkbQ{0@&i3pPxRJD;iWGkml=i(;)h0RHZtmx z$f?s~rfGm=JbyTCrjFE!O44HTq=qzvdZ_pCR=bc_lA@0Ez(OemZ*u|lU4_xlmf^jB zVUsP1(Y7)+$y6G^?co?__hOSShy&6OC9QCOBW!Yh^VnXRf>Aa>P!8m~Z5IvU1+wuB|m?}lkK|Ih|Dx#yl zfC}8#MfzZ>`haQb#lI7hi1`6aT?PSspQ$nv?SrnHiq3i;GYfMW;!0omny1L>800tK zkk?#7DrOs=Q$>Bk4rX#Y6dqg;kS^#e>(SosWfn(b^$K(3Iok34dOCH{-ps)qa|*p= z5GFhIxPLRI>pyUpTIj0Zp&C`v?ieq=SZrRPx=w{T>K8_E|2VUTPNJ86h?Y)CG;zve zFyEzyYuvw&QUh%R|DPPjdO3{%=M);rPE3+nsLgw|m!+7eeqxDh75cbppsP8HO?n%~ zn)7^C3P7zU1TVGGL14N&4*o004N}J;4K%<4^zq zz}mJOY}?ey*V(ph+qP}nwr$(CZ5#WvGa`|wLDtEl9paqgwUYdj%aT`8yELD)qjb7- zoAj*oy-X>qA{!*TBrho+D}S%3py;dkq^zZEuNX#Z`(^0cR z>(DmP_SUY`$#k5qt8SxSrLU~tX>b|_8P*!!8!2NK<4NOB6EsyfwKGjI?KWLAeKU73 zpSNUK23cNP^I3P=M7A8Zp|(4=@Ak&_b@soGMvfy+sk5E)piAw_;Tq;T>$bVexW~Ge zxSx2Eo(7)1Uco!X`_1R@?eV?#PY%cera&t2JSYj~3l0iirpi<8sh!kQT0s}2JJb8< zQ}h%1ALC|vG56U3JCl8vQ9ne5#)lq-tAw|N&qu06-g5!28aF%ZQf@tWnfo5~Mk7%e zZ516Cofq8|y&ioP{TJ&KyA}HuuOHu($e&o3SeNuBTP3F@AEYXzCZ|56OQpM~zwnj$ z=6r8{EWeQ7%%9@#@gD`1fQ3AniwLWPgTf8rJx~G);VUFTZp5Ras5a_?#-f>M13HfGqHkD_S)3DB!Yy!rJPohGhwyFu ziD(E#a**<*IcZP2lm28l8BeB@`D8g+Pqvf&{{aC(rvQ=A7qrLuCdYKg`5|6<{7|AyhbkFTq^WbxJ-6IR z&M{Duq{x#{Wui!oYh_>LCFi)OSG8f4peC!*l#g?*PKl_jF4L404e~^&;veqS$WWp< z#JuK?2VVG?GI&|IzgqerR7sh4ZX~UtTx(L6#WYQ*Dp4h+X`YOF35wK6$ZoJ=;)g%% zc;v5!__uv7d^3gs004N}V_;?gga26!DGXQu04g5=(Exbb+Q6f|fkB&L6Qcl=HsdBi zMj*Le(8@qnT7ZE=n_)Ynhqa!f2#~p*DZ;}-1I%I$ayC&F1F=}*d~LKPK`d4?9VJPA z5R0ua*3C#7#A2UV9%H2pVsX@^2UtphSezlQW@=zlxXKc|&169=?&cIfh&rB|o4S&1 zKrCJn0Uk~^5R1>oNL@w{#Nu}_(Ub-26$o&IxQbzu5H~x}0}8ISnG!C5ishIJKo|yq z=&J(u4k->QArg=f2^oO75-NrFVgxBjD7ocNccHbMVKAWAa>W4-6CPO5YPmzfV8AB4 Z@IjO83?{s=KqX^`zGvsj4^tdfk^ttt3T6NR literal 0 HcmV?d00001 diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ebe1795f85a661c205e4a4612eaf47d56273e68e GIT binary patch literal 15688 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 Date: Wed, 6 Oct 2021 12:01:07 +0200 Subject: [PATCH 031/166] Added redirect function to search bar --- CHANGELOG.md | 2 ++ client/src/components/SearchBar/SearchBar.tsx | 28 +++++++++---------- client/src/interfaces/SearchResult.ts | 1 + client/src/utility/index.ts | 3 +- client/src/utility/redirectUrl.ts | 7 +++++ client/src/utility/searchParser.ts | 9 ++++++ 6 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 client/src/utility/redirectUrl.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9384559..3b6ccfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ### v1.7.0 (TBA) +- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67)) - Fixed bug related to creating new apps/bookmarks with custom icon ([#83](https://github.com/pawelmalak/flame/issues/83)) - URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86)) +- Added static fonts ([#94](https://github.com/pawelmalak/flame/issues/94)) ### v1.6.7 (2021-10-04) - Add multiple labels to Docker Compose ([#90](https://github.com/pawelmalak/flame/issues/90)) diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index f2ccdec..887a2ef 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -11,7 +11,7 @@ import { NewNotification } from '../../interfaces'; import classes from './SearchBar.module.css'; // Utils -import { searchParser } from '../../utility'; +import { searchParser, urlParser, redirectUrl } from '../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; @@ -28,28 +28,28 @@ const SearchBar = (props: ComponentProps): JSX.Element => { }, []); const searchHandler = (e: KeyboardEvent) => { - const searchResult = searchParser(inputRef.current.value); + const { isLocal, search, query, isURL, sameTab } = searchParser( + inputRef.current.value + ); - if (searchResult.isLocal) { - setLocalSearch(searchResult.search); + if (isLocal) { + setLocalSearch(search); } if (e.code === 'Enter') { - if (!searchResult.query.prefix) { + if (!query.prefix) { createNotification({ title: 'Error', message: 'Prefix not found', }); - } else if (searchResult.isLocal) { - setLocalSearch(searchResult.search); + } else if (isURL) { + const url = urlParser(inputRef.current.value)[1]; + redirectUrl(url, sameTab); + } else if (isLocal) { + setLocalSearch(search); } else { - if (searchResult.sameTab) { - document.location.replace( - `${searchResult.query.template}${searchResult.search}` - ); - } else { - window.open(`${searchResult.query.template}${searchResult.search}`); - } + const url = `${query.template}${search}`; + redirectUrl(url, sameTab); } } }; diff --git a/client/src/interfaces/SearchResult.ts b/client/src/interfaces/SearchResult.ts index 271bdc2..3d6c8ae 100644 --- a/client/src/interfaces/SearchResult.ts +++ b/client/src/interfaces/SearchResult.ts @@ -2,6 +2,7 @@ import { Query } from './Query'; export interface SearchResult { isLocal: boolean; + isURL: boolean; sameTab: boolean; search: string; query: Query; diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 99f8d69..caff9c3 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -3,4 +3,5 @@ export * from './urlParser'; export * from './searchConfig'; export * from './checkVersion'; export * from './sortData'; -export * from './searchParser'; \ No newline at end of file +export * from './searchParser'; +export * from './redirectUrl'; diff --git a/client/src/utility/redirectUrl.ts b/client/src/utility/redirectUrl.ts new file mode 100644 index 0000000..81eca10 --- /dev/null +++ b/client/src/utility/redirectUrl.ts @@ -0,0 +1,7 @@ +export const redirectUrl = (url: string, sameTab: boolean) => { + if (sameTab) { + document.location.replace(url); + } else { + window.open(url); + } +}; diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index c477d66..a1c3787 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -6,6 +6,7 @@ import { searchConfig } from '.'; export const searchParser = (searchQuery: string): SearchResult => { const result: SearchResult = { isLocal: false, + isURL: false, sameTab: false, search: '', query: { @@ -15,6 +16,13 @@ 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])$/; + + result.isURL = urlRegex.test(searchQuery); + + // Match prefix and query const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i); const prefix = splitQuery @@ -27,6 +35,7 @@ export const searchParser = (searchQuery: string): SearchResult => { const query = queries.find((q: Query) => q.prefix === prefix); + // If search provider was found if (query) { result.query = query; result.search = search; From 591824dd0c39af3b206d1b24311a595db50575ea Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Oct 2021 14:15:05 +0200 Subject: [PATCH 032/166] Fetch and use custom search queries --- client/src/App.tsx | 13 +++-- .../SearchSettings/SearchSettings.tsx | 4 +- client/src/store/actions/actionTypes.ts | 53 ++++++++++--------- client/src/store/actions/config.ts | 46 +++++++++++----- client/src/store/reducers/config.ts | 44 +++++++++------ client/src/utility/searchParser.ts | 8 ++- controllers/config.js | 15 +++++- routes/config.js | 22 +++----- utils/File.js | 11 ++-- 9 files changed, 136 insertions(+), 80 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 05db805..9311b4b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import { getConfig, setTheme } from './store/actions'; +import { fetchQueries, getConfig, setTheme } from './store/actions'; import 'external-svg-loader'; // Redux @@ -27,15 +27,18 @@ if (localStorage.theme) { // Check for updates checkVersion(); +// fetch queries +store.dispatch(fetchQueries()); + const App = (): JSX.Element => { return ( - - - - + + + + diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index dc3a4af..a3fec29 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -27,6 +27,7 @@ interface Props { createNotification: (notification: NewNotification) => void; updateConfig: (formData: SearchForm) => void; loading: boolean; + customQueries: Query[]; } const SearchSettings = (props: Props): JSX.Element => { @@ -81,7 +82,7 @@ const SearchSettings = (props: Props): JSX.Element => { value={formData.defaultSearchProvider} onChange={(e) => inputChangeHandler(e)} > - {queries.map((query: Query, idx) => ( + {[...queries, ...props.customQueries].map((query: Query, idx) => ( @@ -122,6 +123,7 @@ const SearchSettings = (props: Props): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { loading: state.config.loading, + customQueries: state.config.customQueries, }; }; diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 4324834..0c1cc87 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -26,8 +26,9 @@ import { ClearNotificationAction, // Config GetConfigAction, - UpdateConfigAction + UpdateConfigAction, } from './'; +import { FetchQueriesAction } from './config'; export enum ActionTypes { // Theme @@ -62,35 +63,37 @@ export enum ActionTypes { clearNotification = 'CLEAR_NOTIFICATION', // Config getConfig = 'GET_CONFIG', - updateConfig = 'UPDATE_CONFIG' + updateConfig = 'UPDATE_CONFIG', + fetchQueries = 'FETCH_QUERIES', } -export type Action = +export type Action = // Theme - SetThemeAction | + | SetThemeAction // Apps - GetAppsAction | - PinAppAction | - AddAppAction | - DeleteAppAction | - UpdateAppAction | - ReorderAppsAction | - SortAppsAction | + | GetAppsAction + | PinAppAction + | AddAppAction + | DeleteAppAction + | UpdateAppAction + | ReorderAppsAction + | SortAppsAction // Categories - GetCategoriesAction | - AddCategoryAction | - PinCategoryAction | - DeleteCategoryAction | - UpdateCategoryAction | - SortCategoriesAction | - ReorderCategoriesAction | + | GetCategoriesAction + | AddCategoryAction + | PinCategoryAction + | DeleteCategoryAction + | UpdateCategoryAction + | SortCategoriesAction + | ReorderCategoriesAction // Bookmarks - AddBookmarkAction | - DeleteBookmarkAction | - UpdateBookmarkAction | + | AddBookmarkAction + | DeleteBookmarkAction + | UpdateBookmarkAction // Notifications - CreateNotificationAction | - ClearNotificationAction | + | CreateNotificationAction + | ClearNotificationAction // Config - GetConfigAction | - UpdateConfigAction; \ No newline at end of file + | GetConfigAction + | UpdateConfigAction + | FetchQueriesAction; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index a14e21e..baddbe5 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; -import { Config, ApiResponse } from '../../interfaces'; +import { Config, ApiResponse, Query } from '../../interfaces'; import { CreateNotificationAction } from './notification'; import { searchConfig } from '../../utility'; @@ -13,18 +13,18 @@ export interface GetConfigAction { export const getConfig = () => async (dispatch: Dispatch) => { try { const res = await axios.get>('/api/config'); - + dispatch({ type: ActionTypes.getConfig, - payload: res.data.data - }) + payload: res.data.data, + }); // Set custom page title if set document.title = searchConfig('customTitle', 'Flame'); } catch (err) { - console.log(err) + console.log(err); } -} +}; export interface UpdateConfigAction { type: ActionTypes.updateConfig; @@ -34,19 +34,41 @@ export interface UpdateConfigAction { export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { try { const res = await axios.put>('/api/config', formData); + dispatch({ type: ActionTypes.createNotification, payload: { title: 'Success', - message: 'Settings updated' - } - }) + message: 'Settings updated', + }, + }); dispatch({ type: ActionTypes.updateConfig, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} \ No newline at end of file +}; + +export interface FetchQueriesAction { + type: ActionTypes.fetchQueries; + payload: Query[]; +} + +export const fetchQueries = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>( + '/api/config/0/queries' + ); + + dispatch({ + type: ActionTypes.fetchQueries, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 071f461..93150e2 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -1,36 +1,50 @@ import { ActionTypes, Action } from '../actions'; -import { Config } from '../../interfaces'; +import { Config, Query } from '../../interfaces'; export interface State { loading: boolean; config: Config[]; + customQueries: Query[]; } const initialState: State = { loading: true, - config: [] -} + config: [], + customQueries: [], +}; const getConfig = (state: State, action: Action): State => { return { + ...state, loading: false, - config: action.payload - } -} + }; +}; const updateConfig = (state: State, action: Action): State => { return { ...state, - config: action.payload - } -} + config: action.payload, + }; +}; + +const fetchQueries = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; const configReducer = (state: State = initialState, action: Action) => { - switch(action.type) { - case ActionTypes.getConfig: return getConfig(state, action); - case ActionTypes.updateConfig: return updateConfig(state, action); - default: return state; + switch (action.type) { + case ActionTypes.getConfig: + return getConfig(state, action); + case ActionTypes.updateConfig: + return updateConfig(state, action); + case ActionTypes.fetchQueries: + return fetchQueries(state, action); + default: + return state; } -} +}; -export default configReducer; \ No newline at end of file +export default configReducer; diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index a1c3787..2befdd2 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -1,6 +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 => { @@ -16,6 +16,8 @@ export const searchParser = (searchQuery: string): SearchResult => { }, }; + const customQueries = store.getState().config.customQueries; + // 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])$/; @@ -33,7 +35,9 @@ export const searchParser = (searchQuery: string): SearchResult => { ? encodeURIComponent(splitQuery[2]) : encodeURIComponent(searchQuery); - const query = queries.find((q: Query) => q.prefix === prefix); + const query = [...queries, ...customQueries].find( + (q: Query) => q.prefix === prefix + ); // If search provider was found if (query) { diff --git a/controllers/config.js b/controllers/config.js index a9768d2..85b209a 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -162,7 +162,7 @@ exports.getCss = asyncWrapper(async (req, res, next) => { // @access Public exports.updateCss = asyncWrapper(async (req, res, next) => { const file = new File(join(__dirname, '../public/flame.css')); - file.write(req.body.styles); + file.write(req.body.styles, false); // Copy file to docker volume fs.copyFileSync( @@ -175,3 +175,16 @@ exports.updateCss = asyncWrapper(async (req, res, next) => { data: {}, }); }); + +// @desc Get custom queries file +// @route GET /api/config/0/queries +// @access Public +exports.getQueries = asyncWrapper(async (req, res, next) => { + const file = new File(join(__dirname, '../data/customQueries.json')); + const content = JSON.parse(file.read()); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); diff --git a/routes/config.js b/routes/config.js index eebf5dd..6abea15 100644 --- a/routes/config.js +++ b/routes/config.js @@ -10,23 +10,15 @@ const { deletePair, updateCss, getCss, + getQueries, } = require('../controllers/config'); -router - .route('/') - .post(createPair) - .get(getAllPairs) - .put(updateValues); +router.route('/').post(createPair).get(getAllPairs).put(updateValues); -router - .route('/:key') - .get(getSinglePair) - .put(updateValue) - .delete(deletePair); +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; \ No newline at end of file +router.route('/0/queries').get(getQueries); + +module.exports = router; diff --git a/utils/File.js b/utils/File.js index 0b2fbdc..f135da8 100644 --- a/utils/File.js +++ b/utils/File.js @@ -3,7 +3,7 @@ const fs = require('fs'); class File { constructor(path) { this.path = path; - this.content = ''; + this.content = null; } read() { @@ -16,10 +16,13 @@ class File { } } - write(data) { + write(data, isJSON) { this.content = data; - fs.writeFileSync(this.path, this.content); + fs.writeFileSync( + this.path, + isJSON ? JSON.stringify(this.content) : this.content + ); } } -module.exports = File; \ No newline at end of file +module.exports = File; From 459523dfd256a6869e04f71c4b02edbeb3562390 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Oct 2021 14:17:31 +0200 Subject: [PATCH 033/166] Changed initial files creation process --- package.json | 7 ++++--- server.js | 27 +++++++++++++++--------- utils/findCss.js | 22 -------------------- utils/init/createFile.js | 32 +++++++++++++++++++++++++++++ utils/init/index.js | 9 ++++++++ utils/{ => init}/initConfig.js | 10 ++++++--- utils/init/initFiles.js | 8 ++++++++ utils/{ => init}/initialConfig.json | 0 utils/init/initialFiles.json | 32 +++++++++++++++++++++++++++++ 9 files changed, 109 insertions(+), 38 deletions(-) delete mode 100644 utils/findCss.js create mode 100644 utils/init/createFile.js create mode 100644 utils/init/index.js rename utils/{ => init}/initConfig.js (78%) create mode 100644 utils/init/initFiles.js rename utils/{ => init}/initialConfig.json (100%) create mode 100644 utils/init/initialFiles.json diff --git a/package.json b/package.json index 40f1646..e2a2501 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "main": "index.js", "scripts": { "start": "node server.js", - "init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css", + "init-server": "echo Instaling server dependencies && npm install", "init-client": "cd client && echo Instaling client dependencies && npm install", - "dev-init": "npm run init-server && npm run init-client", - "dev-server": "nodemon server.js", + "dir-init": "npx mkdirp data public && touch public/flame.css public/customQueries.json", + "dev-init": "npm run dir-init && npm run init-server && npm run init-client", + "dev-server": "nodemon server.js -e js", "dev-client": "npm start --prefix client", "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"", "skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\"" diff --git a/server.js b/server.js index 8b09803..5c1d0fa 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,28 @@ require('dotenv').config(); const http = require('http'); + +// Database const { connectDB } = require('./db'); +const associateModels = require('./models/associateModels'); + +// Server const api = require('./api'); const jobs = require('./utils/jobs'); const Socket = require('./Socket'); const Sockets = require('./Sockets'); -const associateModels = require('./models/associateModels'); -const initConfig = require('./utils/initConfig'); -const findCss = require('./utils/findCss'); + +// Utils +const initApp = require('./utils/init'); const Logger = require('./utils/Logger'); const logger = new Logger(); -const PORT = process.env.PORT || 5005; - (async () => { + const PORT = process.env.PORT || 5005; + + // Init app await connectDB(); await associateModels(); - await initConfig(); - findCss(); + await initApp(); // Create server for Express API and WebSockets const server = http.createServer(); @@ -28,6 +33,8 @@ const PORT = process.env.PORT || 5005; Sockets.registerSocket('weather', weatherSocket); server.listen(PORT, () => { - logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); - }) -})(); \ No newline at end of file + logger.log( + `Server is running on port ${PORT} in ${process.env.NODE_ENV} mode` + ); + }); +})(); diff --git a/utils/findCss.js b/utils/findCss.js deleted file mode 100644 index af85a0a..0000000 --- a/utils/findCss.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('fs'); -const { join } = require('path'); -const Logger = require('./Logger'); -const logger = new Logger(); - -// Check if flame.css exists in mounted docker volume. Create new file if not -const findCss = () => { - const srcPath = join(__dirname, '../data/flame.css'); - const destPath = join(__dirname, '../public/flame.css'); - - if (fs.existsSync(srcPath)) { - fs.copyFileSync(srcPath, destPath); - logger.log('Custom CSS file found'); - return; - } - - logger.log('Creating empty CSS file'); - fs.writeFileSync(destPath, ''); - -} - -module.exports = findCss; \ No newline at end of file diff --git a/utils/init/createFile.js b/utils/init/createFile.js new file mode 100644 index 0000000..b46b4f6 --- /dev/null +++ b/utils/init/createFile.js @@ -0,0 +1,32 @@ +const fs = require('fs'); +const { join } = require('path'); + +const Logger = require('../Logger'); +const logger = new Logger(); + +const createFile = async (file) => { + const { name, msg, template, isJSON, paths } = file; + + const srcPath = join(__dirname, paths.src, name); + const destPath = join(__dirname, paths.dest, name); + + // Check if file exists + if (fs.existsSync(srcPath)) { + fs.copyFileSync(srcPath, destPath); + + if (process.env.NODE_ENV == 'development') { + logger.log(msg.found); + } + + return; + } + + // Create file if not + fs.writeFileSync(destPath, isJSON ? JSON.stringify(template) : template); + + if (process.env.NODE_ENV == 'development') { + logger.log(msg.created); + } +}; + +module.exports = createFile; diff --git a/utils/init/index.js b/utils/init/index.js new file mode 100644 index 0000000..a0e11a1 --- /dev/null +++ b/utils/init/index.js @@ -0,0 +1,9 @@ +const initConfig = require('./initConfig'); +const initFiles = require('./initFiles'); + +const initApp = async () => { + await initConfig(); + await initFiles(); +}; + +module.exports = initApp; diff --git a/utils/initConfig.js b/utils/init/initConfig.js similarity index 78% rename from utils/initConfig.js rename to utils/init/initConfig.js index b6a5a18..83ce4ea 100644 --- a/utils/initConfig.js +++ b/utils/init/initConfig.js @@ -1,7 +1,8 @@ const { Op } = require('sequelize'); -const Config = require('../models/Config'); +const Config = require('../../models/Config'); const { config } = require('./initialConfig.json'); -const Logger = require('./Logger'); + +const Logger = require('../Logger'); const logger = new Logger(); const initConfig = async () => { @@ -28,7 +29,10 @@ const initConfig = async () => { } }); - logger.log('Initial config created'); + if (process.env.NODE_ENV == 'development') { + logger.log('Initial config created'); + } + return; }; diff --git a/utils/init/initFiles.js b/utils/init/initFiles.js new file mode 100644 index 0000000..cee54ca --- /dev/null +++ b/utils/init/initFiles.js @@ -0,0 +1,8 @@ +const createFile = require('./createFile'); +const { files } = require('./initialFiles.json'); + +const initFiles = async () => { + files.forEach(async (file) => await createFile(file)); +}; + +module.exports = initFiles; diff --git a/utils/initialConfig.json b/utils/init/initialConfig.json similarity index 100% rename from utils/initialConfig.json rename to utils/init/initialConfig.json diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json new file mode 100644 index 0000000..42354d7 --- /dev/null +++ b/utils/init/initialFiles.json @@ -0,0 +1,32 @@ +{ + "files": [ + { + "name": "flame.css", + "msg": { + "created": "Created empty CSS file", + "found": "Custom CSS file found" + }, + "paths": { + "src": "../../data", + "dest": "../../public" + }, + "template": "", + "isJSON": false + }, + { + "name": "customQueries.json", + "msg": { + "created": "Created empty queries file", + "found": "Custom queries file found" + }, + "paths": { + "src": "../../data", + "dest": "../../data" + }, + "template": { + "queries": [] + }, + "isJSON": true + } + ] +} From 231dbc4577b795c2b6c8cafaee35d5c5d9fbe26a Mon Sep 17 00:00:00 2001 From: Pablo Ruiz Date: Wed, 6 Oct 2021 22:17:43 +0200 Subject: [PATCH 034/166] Added remote docker host --- .../Settings/OtherSettings/OtherSettings.tsx | 13 ++++++++++++ client/src/interfaces/Forms.ts | 1 + controllers/apps.js | 21 ++++++++++++------- utils/initialConfig.json | 4 ++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index afaf072..bd9bf49 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -52,6 +52,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { bookmarksSameTab: 0, searchSameTab: 0, dockerApps: 1, + dockerHost: 'localhost', kubernetesApps: 1, unpinStoppedApps: 1, }); @@ -72,6 +73,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { bookmarksSameTab: searchConfig('bookmarksSameTab', 0), searchSameTab: searchConfig('searchSameTab', 0), dockerApps: searchConfig('dockerApps', 0), + dockerHost: searchConfig('dockerHost', 'localhost'), kubernetesApps: searchConfig('kubernetesApps', 0), unpinStoppedApps: searchConfig('unpinStoppedApps', 0), }); @@ -275,6 +277,17 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { {/* DOCKER SETTINGS */}

Docker

+ + + inputChangeHandler(e)} + /> + { {/* BEAHVIOR OPTIONS */} -

App Behavior

+ {/* MODULES OPTIONS */} -

Modules

+ { {/* KUBERNETES SETTINGS */} -

Kubernetes

+ inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + + + ); +}; + +export default QueriesForm; diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index a3fec29..5b40f71 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -1,5 +1,5 @@ // React -import { useState, useEffect, FormEvent, ChangeEvent } from 'react'; +import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react'; import { connect } from 'react-redux'; // State @@ -13,15 +13,19 @@ import { SearchForm, } from '../../../interfaces'; -// Utils -import { searchConfig } from '../../../utility'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; - -// Data -import { queries } from '../../../utility/searchQueries.json'; +// Components +import CustomQueries from './CustomQueries/CustomQueries'; // UI import Button from '../../UI/Buttons/Button/Button'; +import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; + +// Utils +import { searchConfig } from '../../../utility'; + +// Data +import { queries } from '../../../utility/searchQueries.json'; interface Props { createNotification: (notification: NewNotification) => void; @@ -73,50 +77,65 @@ const SearchSettings = (props: Props): JSX.Element => { }; return ( -
formSubmitHandler(e)}> - - - - - - - - - - - - - -
+ + {/* GENERAL SETTINGS */} +
formSubmitHandler(e)} + style={{ marginBottom: '30px' }} + > + + + + + + + + + + + + + + + + + {/* CUSTOM QUERIES */} + + +
); }; diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.module.css b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css similarity index 89% rename from client/src/components/Settings/OtherSettings/OtherSettings.module.css rename to client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css index 36e4deb..137667c 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.module.css +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css @@ -1,4 +1,4 @@ -.SettingsSection { +.SettingsHeadline { color: var(--color-primary); padding-bottom: 3px; margin-bottom: 10px; @@ -6,4 +6,4 @@ font-weight: 500; border-bottom: 2px solid var(--color-accent); display: inline-block; -} \ No newline at end of file +} diff --git a/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx new file mode 100644 index 0000000..5d14949 --- /dev/null +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx @@ -0,0 +1,11 @@ +const classes = require('./SettingsHeadline.module.css'); + +interface Props { + text: string; +} + +const SettingsHeadline = (props: Props): JSX.Element => { + return

{props.text}

; +}; + +export default SettingsHeadline; diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 0c1cc87..f040016 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -28,7 +28,11 @@ import { GetConfigAction, UpdateConfigAction, } from './'; -import { FetchQueriesAction } from './config'; +import { + AddQueryAction, + DeleteQueryAction, + FetchQueriesAction, +} from './config'; export enum ActionTypes { // Theme @@ -65,6 +69,8 @@ export enum ActionTypes { getConfig = 'GET_CONFIG', updateConfig = 'UPDATE_CONFIG', fetchQueries = 'FETCH_QUERIES', + addQuery = 'ADD_QUERY', + deleteQuery = 'DELETE_QUERY', } export type Action = @@ -96,4 +102,6 @@ export type Action = // Config | GetConfigAction | UpdateConfigAction - | FetchQueriesAction; + | FetchQueriesAction + | AddQueryAction + | DeleteQueryAction; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index baddbe5..d0c8a42 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -60,9 +60,7 @@ export interface FetchQueriesAction { export const fetchQueries = () => async (dispatch: Dispatch) => { try { - const res = await axios.get>( - '/api/config/0/queries' - ); + const res = await axios.get>('/api/queries'); dispatch({ type: ActionTypes.fetchQueries, @@ -72,3 +70,43 @@ export const fetchQueries = console.log(err); } }; + +export interface AddQueryAction { + type: ActionTypes.addQuery; + payload: Query; +} + +export const addQuery = + (query: Query) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>('/api/queries', query); + + dispatch({ + type: ActionTypes.addQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export interface DeleteQueryAction { + type: ActionTypes.deleteQuery; + payload: Query[]; +} + +export const deleteQuery = + (prefix: string) => async (dispatch: Dispatch) => { + try { + const res = await axios.delete>( + `/api/queries/${prefix}` + ); + + dispatch({ + type: ActionTypes.deleteQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 93150e2..08b68e6 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -34,6 +34,20 @@ const fetchQueries = (state: State, action: Action): State => { }; }; +const addQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: [...state.customQueries, action.payload], + }; +}; + +const deleteQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; + const configReducer = (state: State = initialState, action: Action) => { switch (action.type) { case ActionTypes.getConfig: @@ -42,6 +56,10 @@ const configReducer = (state: State = initialState, action: Action) => { return updateConfig(state, action); case ActionTypes.fetchQueries: return fetchQueries(state, action); + case ActionTypes.addQuery: + return addQuery(state, action); + case ActionTypes.deleteQuery: + return deleteQuery(state, action); default: return state; } diff --git a/controllers/config.js b/controllers/config.js index 85b209a..e5290aa 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -175,16 +175,3 @@ exports.updateCss = asyncWrapper(async (req, res, next) => { data: {}, }); }); - -// @desc Get custom queries file -// @route GET /api/config/0/queries -// @access Public -exports.getQueries = asyncWrapper(async (req, res, next) => { - const file = new File(join(__dirname, '../data/customQueries.json')); - const content = JSON.parse(file.read()); - - res.status(200).json({ - success: true, - data: content.queries, - }); -}); diff --git a/controllers/queries/index.js b/controllers/queries/index.js new file mode 100644 index 0000000..b68f145 --- /dev/null +++ b/controllers/queries/index.js @@ -0,0 +1,53 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); +const { join } = require('path'); + +const QUERIES_PATH = join(__dirname, '../../data/customQueries.json'); + +// @desc Add custom search query +// @route POST /api/queries +// @access Public +exports.addQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + // Add new query + content.queries.push(req.body); + file.write(content, true); + + res.status(201).json({ + success: true, + data: req.body, + }); +}); + +// @desc Get custom queries file +// @route GET /api/queries +// @access Public +exports.getQueries = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + const content = JSON.parse(file.read()); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +// @desc Delete query +// @route DELETE /api/queries/:prefix +// @access Public +exports.deleteQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + content.queries = content.queries.filter( + (q) => q.prefix != req.params.prefix + ); + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); diff --git a/routes/config.js b/routes/config.js index 6abea15..8c9ac15 100644 --- a/routes/config.js +++ b/routes/config.js @@ -10,7 +10,6 @@ const { deletePair, updateCss, getCss, - getQueries, } = require('../controllers/config'); router.route('/').post(createPair).get(getAllPairs).put(updateValues); @@ -19,6 +18,4 @@ router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair); router.route('/0/css').get(getCss).put(updateCss); -router.route('/0/queries').get(getQueries); - module.exports = router; diff --git a/routes/queries.js b/routes/queries.js new file mode 100644 index 0000000..0e17bc0 --- /dev/null +++ b/routes/queries.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); + +const { + getQueries, + addQuery, + deleteQuery, +} = require('../controllers/queries/'); + +router.route('/').post(addQuery).get(getQueries); +router.route('/:prefix').delete(deleteQuery); + +module.exports = router; From 38ffdf1bff3c9c00fde9439c0e93f14a820042eb Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 11 Oct 2021 13:55:53 +0200 Subject: [PATCH 040/166] Add and update custom queries --- .../CustomQueries/CustomQueries.module.css | 4 + .../CustomQueries/CustomQueries.tsx | 26 ++++-- .../CustomQueries/QueriesForm.tsx | 86 +++++++++++++++---- client/src/store/actions/actionTypes.ts | 5 +- client/src/store/actions/config.ts | 23 +++++ client/src/store/reducers/config.ts | 9 ++ controllers/queries/index.js | 28 ++++++ routes/queries.js | 3 +- 8 files changed, 159 insertions(+), 25 deletions(-) diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.module.css b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.module.css index 36313bf..73297cc 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.module.css +++ b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.module.css @@ -7,6 +7,10 @@ color: var(--color-primary); } +.QueriesGrid span:last-child { + margin-bottom: 10px; +} + .ActionIcons { display: flex; } diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx index bc50cf4..de9d226 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx @@ -3,11 +3,9 @@ import { connect } from 'react-redux'; import classes from './CustomQueries.module.css'; -import ModalForm from '../../../UI/Forms/ModalForm/ModalForm'; import Modal from '../../../UI/Modal/Modal'; import Icon from '../../../UI/Icons/Icon/Icon'; import { GlobalState, Query } from '../../../../interfaces'; -import InputGroup from '../../../UI/Forms/InputGroup/InputGroup'; import QueriesForm from './QueriesForm'; import { deleteQuery } from '../../../../store/actions'; import Button from '../../../UI/Buttons/Button/Button'; @@ -21,6 +19,12 @@ const CustomQueries = (props: Props): JSX.Element => { const { customQueries, deleteQuery } = props; const [modalIsOpen, setModalIsOpen] = useState(false); + const [editableQuery, setEditableQuery] = useState(null); + + const updateHandler = (query: Query) => { + setEditableQuery(query); + setModalIsOpen(true); + }; const deleteHandler = (query: Query) => { if (window.confirm(`Are you sure you want to delete this provider?`)) { @@ -34,7 +38,14 @@ const CustomQueries = (props: Props): JSX.Element => { isOpen={modalIsOpen} setIsOpen={() => setModalIsOpen(!modalIsOpen)} > - setModalIsOpen(!modalIsOpen)} /> + {editableQuery ? ( + setModalIsOpen(!modalIsOpen)} + query={editableQuery} + /> + ) : ( + setModalIsOpen(!modalIsOpen)} /> + )}
@@ -54,7 +65,7 @@ const CustomQueries = (props: Props): JSX.Element => { {q.name} {q.prefix} - + updateHandler(q)}> deleteHandler(q)}> @@ -65,7 +76,12 @@ const CustomQueries = (props: Props): JSX.Element => { ))}
-
diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx index 8a20e05..42ad654 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/QueriesForm.tsx @@ -1,20 +1,70 @@ -import { useState } from 'react'; +import { ChangeEvent, FormEvent, useState, useEffect } from 'react'; +import { Query } from '../../../../interfaces'; import Button from '../../../UI/Buttons/Button/Button'; import InputGroup from '../../../UI/Forms/InputGroup/InputGroup'; import ModalForm from '../../../UI/Forms/ModalForm/ModalForm'; +import { connect } from 'react-redux'; +import { addQuery, updateQuery } from '../../../../store/actions'; interface Props { modalHandler: () => void; - // addApp: (formData: NewApp | FormData) => any; - // updateApp: (id: number, formData: NewApp | FormData) => any; - // app?: App; + addQuery: (query: Query) => {}; + updateQuery: (query: Query, Oldprefix: string) => {}; + query?: Query; } const QueriesForm = (props: Props): JSX.Element => { - const [formData, setFormData] = useState(); + const { modalHandler, addQuery, updateQuery, query } = props; + + const [formData, setFormData] = useState({ + name: '', + prefix: '', + template: '', + }); + + const inputChangeHandler = (e: ChangeEvent) => { + const { name, value } = e.target; + + setFormData({ + ...formData, + [name]: value, + }); + }; + + const formHandler = (e: FormEvent) => { + e.preventDefault(); + + if (query) { + updateQuery(formData, query.prefix); + } else { + addQuery(formData); + } + + // close modal + modalHandler(); + + // clear form + setFormData({ + name: '', + prefix: '', + template: '', + }); + }; + + useEffect(() => { + if (query) { + setFormData(query); + } else { + setFormData({ + name: '', + prefix: '', + template: '', + }); + } + }, [query]); return ( - {}}> + { id="name" placeholder="Google" required - // value={formData.name} - // onChange={(e) => inputChangeHandler(e)} + value={formData.name} + onChange={(e) => inputChangeHandler(e)} /> inputChangeHandler(e)} + value={formData.prefix} + onChange={(e) => inputChangeHandler(e)} /> inputChangeHandler(e)} + value={formData.template} + onChange={(e) => inputChangeHandler(e)} /> - + {query ? : } ); }; -export default QueriesForm; +export default connect(null, { addQuery, updateQuery })(QueriesForm); diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index f040016..c670b2f 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -32,6 +32,7 @@ import { AddQueryAction, DeleteQueryAction, FetchQueriesAction, + UpdateQueryAction, } from './config'; export enum ActionTypes { @@ -71,6 +72,7 @@ export enum ActionTypes { fetchQueries = 'FETCH_QUERIES', addQuery = 'ADD_QUERY', deleteQuery = 'DELETE_QUERY', + updateQuery = 'UPDATE_QUERY', } export type Action = @@ -104,4 +106,5 @@ export type Action = | UpdateConfigAction | FetchQueriesAction | AddQueryAction - | DeleteQueryAction; + | DeleteQueryAction + | UpdateQueryAction; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index d0c8a42..29c5186 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -110,3 +110,26 @@ export const deleteQuery = console.log(err); } }; + +export interface UpdateQueryAction { + type: ActionTypes.updateQuery; + payload: Query[]; +} + +export const updateQuery = + (query: Query, oldPrefix: string) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/queries/${oldPrefix}`, + query + ); + + dispatch({ + type: ActionTypes.updateQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 08b68e6..ac81aeb 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -48,6 +48,13 @@ const deleteQuery = (state: State, action: Action): State => { }; }; +const updateQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; + const configReducer = (state: State = initialState, action: Action) => { switch (action.type) { case ActionTypes.getConfig: @@ -60,6 +67,8 @@ const configReducer = (state: State = initialState, action: Action) => { return addQuery(state, action); case ActionTypes.deleteQuery: return deleteQuery(state, action); + case ActionTypes.updateQuery: + return updateQuery(state, action); default: return state; } diff --git a/controllers/queries/index.js b/controllers/queries/index.js index b68f145..ae1ccec 100644 --- a/controllers/queries/index.js +++ b/controllers/queries/index.js @@ -34,6 +34,34 @@ exports.getQueries = asyncWrapper(async (req, res, next) => { }); }); +// @desc Update query +// @route PUT /api/queries/:prefix +// @access Public +exports.updateQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + let queryIdx = content.queries.findIndex( + (q) => q.prefix == req.params.prefix + ); + + // query found + if (queryIdx > -1) { + content.queries = [ + ...content.queries.slice(0, queryIdx), + req.body, + ...content.queries.slice(queryIdx + 1), + ]; + } + + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + // @desc Delete query // @route DELETE /api/queries/:prefix // @access Public diff --git a/routes/queries.js b/routes/queries.js index 0e17bc0..afacffd 100644 --- a/routes/queries.js +++ b/routes/queries.js @@ -5,9 +5,10 @@ const { getQueries, addQuery, deleteQuery, + updateQuery, } = require('../controllers/queries/'); router.route('/').post(addQuery).get(getQueries); -router.route('/:prefix').delete(deleteQuery); +router.route('/:prefix').delete(deleteQuery).put(updateQuery); module.exports = router; From edb04c375f841d0fa8d9559f430193b8e15ee416 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 11 Oct 2021 14:05:53 +0200 Subject: [PATCH 041/166] Prevent deleting active search provider --- .../CustomQueries/CustomQueries.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx index de9d226..c5dac62 100644 --- a/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx +++ b/client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx @@ -5,18 +5,20 @@ import classes from './CustomQueries.module.css'; import Modal from '../../../UI/Modal/Modal'; import Icon from '../../../UI/Icons/Icon/Icon'; -import { GlobalState, Query } from '../../../../interfaces'; +import { GlobalState, NewNotification, Query } from '../../../../interfaces'; import QueriesForm from './QueriesForm'; -import { deleteQuery } from '../../../../store/actions'; +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; } const CustomQueries = (props: Props): JSX.Element => { - const { customQueries, deleteQuery } = props; + const { customQueries, deleteQuery, createNotification } = props; const [modalIsOpen, setModalIsOpen] = useState(false); const [editableQuery, setEditableQuery] = useState(null); @@ -27,7 +29,17 @@ const CustomQueries = (props: Props): JSX.Element => { }; const deleteHandler = (query: Query) => { - if (window.confirm(`Are you sure you want to delete this provider?`)) { + const currentProvider = searchConfig('defaultSearchProvider', 'l'); + const isCurrent = currentProvider === query.prefix; + + if (isCurrent) { + createNotification({ + title: 'Error', + message: 'Cannot delete active provider', + }); + } else if ( + window.confirm(`Are you sure you want to delete this provider?`) + ) { deleteQuery(query.prefix); } }; @@ -95,4 +107,6 @@ const mapStateToProps = (state: GlobalState) => { }; }; -export default connect(mapStateToProps, { deleteQuery })(CustomQueries); +export default connect(mapStateToProps, { deleteQuery, createNotification })( + CustomQueries +); From 7129fe83dae086bad49f935885b831083e85abfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Mon, 11 Oct 2021 15:15:26 +0200 Subject: [PATCH 042/166] Fixed bug with fetching config. Pushed version 1.7.0 --- .env | 2 +- CHANGELOG.md | 4 +++- client/.env | 2 +- .../components/Settings/SearchSettings/SearchSettings.tsx | 2 +- client/src/store/reducers/config.ts | 1 + controllers/apps.js | 5 ++++- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.env b/.env index feb0686..1bb2edb 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.6.9 \ No newline at end of file +VERSION=1.7.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 85af84f..54c68e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ -### v1.7.0 (TBA) +### 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)) - Fixed bug related to creating new apps/bookmarks with custom icon ([#83](https://github.com/pawelmalak/flame/issues/83)) - URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86)) - Added static fonts ([#94](https://github.com/pawelmalak/flame/issues/94)) +- Fixed bug with overriding app icon created with docker labels ### v1.6.9 (2021-10-09) - Added option for remote docker host ([#97](https://github.com/pawelmalak/flame/issues/97)) diff --git a/client/.env b/client/.env index e9f8924..6dbe18b 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.9 \ No newline at end of file +REACT_APP_VERSION=1.7.0 \ No newline at end of file diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index 5b40f71..b2ac422 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -46,7 +46,7 @@ const SearchSettings = (props: Props): JSX.Element => { useEffect(() => { setFormData({ hideSearch: searchConfig('hideSearch', 0), - defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'), + defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'), searchSameTab: searchConfig('searchSameTab', 0), }); }, [props.loading]); diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index ac81aeb..ae2699e 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -17,6 +17,7 @@ const getConfig = (state: State, action: Action): State => { return { ...state, loading: false, + config: action.payload, }; }; diff --git a/controllers/apps.js b/controllers/apps.js index c2b065e..b976d45 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -131,7 +131,10 @@ exports.getApps = asyncWrapper(async (req, res, next) => { 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')) { + if ( + item.icon === 'custom' || + (item.icon === 'docker' && app.icon != 'docker') + ) { await app.update({ name: item.name, url: item.url, 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 043/166] 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 044/166] 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 045/166] 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 046/166] 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 047/166] 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 048/166] 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 049/166] 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, From df6d96f5b697c8a149a9c5ccba9a5ce70a95918d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 26 Oct 2021 13:09:42 +0200 Subject: [PATCH 050/166] Added option to disable search bar autofocus --- DEV_GUIDELINES.md | 10 +++++++++ client/src/components/SearchBar/SearchBar.tsx | 21 ++++++++++++++----- .../SearchSettings/SearchSettings.tsx | 12 +++++++++++ client/src/interfaces/Config.ts | 1 + client/src/interfaces/Forms.ts | 1 + .../utility/templateObjects/configTemplate.ts | 1 + .../templateObjects/settingsTemplate.ts | 1 + utils/init/initialConfig.json | 4 +++- 8 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 DEV_GUIDELINES.md diff --git a/DEV_GUIDELINES.md b/DEV_GUIDELINES.md new file mode 100644 index 0000000..462a17f --- /dev/null +++ b/DEV_GUIDELINES.md @@ -0,0 +1,10 @@ +## Adding new config key + +1. Edit utils/init/initialConfig.json +2. Edit client/src/interfaces/Config.ts +3. Edit client/src/utility/templateObjects/configTemplate.ts + +If config value will be used in a form: + +4. Edit client/src/interfaces/Forms.ts +5. Edit client/src/utility/templateObjects/settingsTemplate.ts \ No newline at end of file diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index b6a981f..a535c19 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { createNotification } from '../../store/actions'; // Typescript -import { NewNotification } from '../../interfaces'; +import { Config, GlobalState, NewNotification } from '../../interfaces'; // CSS import classes from './SearchBar.module.css'; @@ -16,16 +16,20 @@ import { searchParser, urlParser, redirectUrl } from '../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; setLocalSearch: (query: string) => void; + config: Config; + loading: boolean; } const SearchBar = (props: ComponentProps): JSX.Element => { - const { setLocalSearch, createNotification } = props; + const { setLocalSearch, createNotification, config, loading } = props; const inputRef = useRef(document.createElement('input')); useEffect(() => { - inputRef.current.focus(); - }, []); + if (!loading && !config.disableAutofocus) { + inputRef.current.focus(); + } + }, [config]); const clearSearch = () => { inputRef.current.value = ''; @@ -78,4 +82,11 @@ const SearchBar = (props: ComponentProps): JSX.Element => { ); }; -export default connect(null, { createNotification })(SearchBar); +const mapStateToProps = (state: GlobalState) => { + return { + config: state.config.config, + loading: state.config.loading, + }; +}; + +export default connect(mapStateToProps, { createNotification })(SearchBar); diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx index a403fa6..d05def5 100644 --- a/client/src/components/Settings/SearchSettings/SearchSettings.tsx +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -121,6 +121,18 @@ const SearchSettings = (props: Props): JSX.Element => { + + + + diff --git a/client/src/interfaces/Config.ts b/client/src/interfaces/Config.ts index 1b60ca7..88f1d5c 100644 --- a/client/src/interfaces/Config.ts +++ b/client/src/interfaces/Config.ts @@ -20,4 +20,5 @@ export interface Config { kubernetesApps: boolean; unpinStoppedApps: boolean; useAmericanDate: boolean; + disableAutofocus: boolean; } diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 411ce90..6e144bb 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -9,6 +9,7 @@ export interface SearchForm { hideSearch: boolean; defaultSearchProvider: string; searchSameTab: boolean; + disableAutofocus: boolean; } export interface OtherSettingsForm { diff --git a/client/src/utility/templateObjects/configTemplate.ts b/client/src/utility/templateObjects/configTemplate.ts index 4d4843f..a6f590a 100644 --- a/client/src/utility/templateObjects/configTemplate.ts +++ b/client/src/utility/templateObjects/configTemplate.ts @@ -22,4 +22,5 @@ export const configTemplate: Config = { kubernetesApps: false, unpinStoppedApps: false, useAmericanDate: false, + disableAutofocus: false, }; diff --git a/client/src/utility/templateObjects/settingsTemplate.ts b/client/src/utility/templateObjects/settingsTemplate.ts index 05bc887..30fa871 100644 --- a/client/src/utility/templateObjects/settingsTemplate.ts +++ b/client/src/utility/templateObjects/settingsTemplate.ts @@ -28,4 +28,5 @@ export const searchSettingsTemplate: SearchForm = { hideSearch: false, searchSameTab: false, defaultSearchProvider: 'l', + disableAutofocus: false, }; diff --git a/utils/init/initialConfig.json b/utils/init/initialConfig.json index f6b57a3..11a839a 100644 --- a/utils/init/initialConfig.json +++ b/utils/init/initialConfig.json @@ -18,5 +18,7 @@ "dockerApps": false, "dockerHost": "localhost", "kubernetesApps": false, - "unpinStoppedApps": false + "unpinStoppedApps": false, + "useAmericanDate": false, + "disableAutofocus": false } From 3d3e2eed8c679a0a88cac349a58dd31cc3f7bd7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Tue, 26 Oct 2021 14:37:01 +0200 Subject: [PATCH 051/166] Fixed bug with weather logging. Fixed bug with search bar shortcuts --- CHANGELOG.md | 5 +++ client/src/components/SearchBar/SearchBar.tsx | 16 +++++++++ utils/getExternalWeather.js | 8 ----- utils/jobs.js | 35 +++++++++++++------ 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2dbd5..0f57ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### v1.7.2 (TBA) +- Use search bar shortcuts when it's not focused ([#124](https://github.com/pawelmalak/flame/issues/124)) +- Fixed bug with Weather API still logging with module being disabled ([#125](https://github.com/pawelmalak/flame/issues/125)) +- Added option to disable search bar autofocus ([#127](https://github.com/pawelmalak/flame/issues/127)) + ### 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)) diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index a535c19..c20b457 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -25,12 +25,28 @@ const SearchBar = (props: ComponentProps): JSX.Element => { const inputRef = useRef(document.createElement('input')); + // Search bar autofocus useEffect(() => { if (!loading && !config.disableAutofocus) { inputRef.current.focus(); } }, [config]); + // Listen for keyboard events outside of search bar + useEffect(() => { + const keyOutsideFocus = (e: any) => { + const { key } = e as KeyboardEvent; + + if (key === 'Escape') { + clearSearch(); + } + }; + + window.addEventListener('keydown', keyOutsideFocus); + + return () => window.removeEventListener('keydown', keyOutsideFocus); + }, []); + const clearSearch = () => { inputRef.current.value = ''; setLocalSearch(''); diff --git a/utils/getExternalWeather.js b/utils/getExternalWeather.js index 8b2be8d..20edac4 100644 --- a/utils/getExternalWeather.js +++ b/utils/getExternalWeather.js @@ -5,14 +5,6 @@ const loadConfig = require('./loadConfig'); const getExternalWeather = async () => { const { WEATHER_API_KEY: secret, lat, long } = await loadConfig(); - if (!secret) { - throw new Error('API key was not found. Weather updated failed'); - } - - if (!lat || !long) { - throw new Error('Location was not found. Weather updated failed'); - } - // Fetch data from external API try { const res = await axios.get( diff --git a/utils/jobs.js b/utils/jobs.js index 935f497..9716af0 100644 --- a/utils/jobs.js +++ b/utils/jobs.js @@ -3,20 +3,33 @@ const getExternalWeather = require('./getExternalWeather'); const clearWeatherData = require('./clearWeatherData'); const Sockets = require('../Sockets'); const Logger = require('./Logger'); +const loadConfig = require('./loadConfig'); const logger = new Logger(); // Update weather data every 15 minutes -const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => { - try { - const weatherData = await getExternalWeather(); - logger.log('Weather updated'); - Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData)); - } catch (err) { - logger.log(err.message, 'ERROR'); +const weatherJob = schedule.scheduleJob( + 'updateWeather', + '0 */15 * * * *', + async () => { + const { WEATHER_API_KEY: secret } = await loadConfig(); + + try { + const weatherData = await getExternalWeather(); + logger.log('Weather updated'); + Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData)); + } catch (err) { + if (secret) { + logger.log(err.message, 'ERROR'); + } + } } -}) +); // Clear old weather data every 4 hours -const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 5 */4 * * *', async () => { - clearWeatherData(); -}) \ No newline at end of file +const weatherCleanerJob = schedule.scheduleJob( + 'clearWeather', + '0 5 */4 * * *', + async () => { + clearWeatherData(); + } +); From da13ca6092c2e41b77138654f1f87fe089a92842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 27 Oct 2021 11:52:57 +0200 Subject: [PATCH 052/166] Search bar redirect to local search results --- CHANGELOG.md | 1 + client/src/components/Home/Home.tsx | 55 ++++++++++++------- client/src/components/SearchBar/SearchBar.tsx | 27 +++++++-- client/src/utility/redirectUrl.ts | 8 ++- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f57ca1..3eae525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### v1.7.2 (TBA) +- Pressing Enter while search bar is focused will now redirect to first result of local search ([#121](https://github.com/pawelmalak/flame/issues/121)) - Use search bar shortcuts when it's not focused ([#124](https://github.com/pawelmalak/flame/issues/124)) - Fixed bug with Weather API still logging with module being disabled ([#125](https://github.com/pawelmalak/flame/issues/125)) - Added option to disable search bar autofocus ([#127](https://github.com/pawelmalak/flame/issues/127)) diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 18d81bc..4a0adbe 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -55,17 +55,21 @@ const Home = (props: ComponentProps): JSX.Element => { // Local search query const [localSearch, setLocalSearch] = useState(null); + const [appSearchResult, setAppSearchResult] = useState(null); + const [bookmarkSearchResult, setBookmarkSearchResult] = useState< + null | Category[] + >(null); // Load applications useEffect(() => { - if (apps.length === 0) { + if (!apps.length) { getApps(); } }, [getApps]); // Load bookmark categories useEffect(() => { - if (categories.length === 0) { + if (!categories.length) { getCategories(); } }, [getCategories]); @@ -87,22 +91,37 @@ const Home = (props: ComponentProps): JSX.Element => { return () => clearInterval(interval); }, []); - // Search bookmarks - const searchBookmarks = (query: string): Category[] => { - const category = { ...categories[0] }; - category.name = 'Search Results'; - category.bookmarks = categories - .map(({ bookmarks }) => bookmarks) - .flat() - .filter(({ name }) => new RegExp(query, 'i').test(name)); + useEffect(() => { + if (localSearch) { + // Search through apps + setAppSearchResult([ + ...apps.filter(({ name }) => new RegExp(localSearch, 'i').test(name)), + ]); - return [category]; - }; + // Search through bookmarks + const category = { ...categories[0] }; + + category.name = 'Search Results'; + category.bookmarks = categories + .map(({ bookmarks }) => bookmarks) + .flat() + .filter(({ name }) => new RegExp(localSearch, 'i').test(name)); + + setBookmarkSearchResult([category]); + } else { + setAppSearchResult(null); + setBookmarkSearchResult(null); + } + }, [localSearch]); return ( {!props.config.hideSearch ? ( - + ) : (
)} @@ -130,11 +149,9 @@ const Home = (props: ComponentProps): JSX.Element => { ) : ( isPinned) - : apps.filter(({ name }) => - new RegExp(localSearch, 'i').test(name) - ) + : appSearchResult } totalApps={apps.length} searching={!!localSearch} @@ -154,9 +171,9 @@ const Home = (props: ComponentProps): JSX.Element => { ) : ( isPinned) - : searchBookmarks(localSearch) + : bookmarkSearchResult } totalCategories={categories.length} searching={!!localSearch} diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index c20b457..7a91525 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -5,7 +5,13 @@ import { connect } from 'react-redux'; import { createNotification } from '../../store/actions'; // Typescript -import { Config, GlobalState, NewNotification } from '../../interfaces'; +import { + App, + Category, + Config, + GlobalState, + NewNotification, +} from '../../interfaces'; // CSS import classes from './SearchBar.module.css'; @@ -16,12 +22,21 @@ import { searchParser, urlParser, redirectUrl } from '../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; setLocalSearch: (query: string) => void; + appSearchResult: App[] | null; + bookmarkSearchResult: Category[] | null; config: Config; loading: boolean; } const SearchBar = (props: ComponentProps): JSX.Element => { - const { setLocalSearch, createNotification, config, loading } = props; + const { + setLocalSearch, + createNotification, + config, + loading, + appSearchResult, + bookmarkSearchResult, + } = props; const inputRef = useRef(document.createElement('input')); @@ -73,8 +88,12 @@ const SearchBar = (props: ComponentProps): JSX.Element => { const url = urlParser(inputRef.current.value)[1]; redirectUrl(url, sameTab); } else if (isLocal) { - // Local query -> filter apps and bookmarks - setLocalSearch(search); + // Local query -> redirect if at least 1 result found + if (appSearchResult?.length) { + redirectUrl(appSearchResult[0].url, sameTab); + } else if (bookmarkSearchResult?.length) { + redirectUrl(bookmarkSearchResult[0].bookmarks[0].url, sameTab); + } } else { // Valid query -> redirect to search results const url = `${query.template}${search}`; diff --git a/client/src/utility/redirectUrl.ts b/client/src/utility/redirectUrl.ts index 81eca10..533f5d2 100644 --- a/client/src/utility/redirectUrl.ts +++ b/client/src/utility/redirectUrl.ts @@ -1,7 +1,11 @@ +import { urlParser } from '.'; + export const redirectUrl = (url: string, sameTab: boolean) => { + const parsedUrl = urlParser(url)[1]; + if (sameTab) { - document.location.replace(url); + document.location.replace(parsedUrl); } else { - window.open(url); + window.open(parsedUrl); } }; From feb7275cf8648d99b1d2aa1e27ae050cb53285a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 28 Oct 2021 11:44:36 +0200 Subject: [PATCH 053/166] Pushed version 1.7.2 --- .env | 2 +- CHANGELOG.md | 2 +- client/.env | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env b/.env index e2c26fc..3288f33 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.7.1 \ No newline at end of file +VERSION=1.7.2 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eae525..25d796b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### v1.7.2 (TBA) +### v1.7.2 (2021-10-28) - Pressing Enter while search bar is focused will now redirect to first result of local search ([#121](https://github.com/pawelmalak/flame/issues/121)) - Use search bar shortcuts when it's not focused ([#124](https://github.com/pawelmalak/flame/issues/124)) - Fixed bug with Weather API still logging with module being disabled ([#125](https://github.com/pawelmalak/flame/issues/125)) diff --git a/client/.env b/client/.env index 1511942..e16ddf3 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.7.1 \ No newline at end of file +REACT_APP_VERSION=1.7.2 \ No newline at end of file From 88694c7e2767040bcf9551fe65eb3ce80a849368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 28 Oct 2021 16:05:21 +0200 Subject: [PATCH 054/166] Fixed bug with custom css not updating --- .env | 2 +- CHANGELOG.md | 3 +++ client/.env | 2 +- controllers/config/updateCSS.js | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 3288f33..5c6e879 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.7.2 \ No newline at end of file +VERSION=1.7.3 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d796b..7c870d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.7.3 (2021-10-28) +- Fixed bug with custom CSS not updating + ### v1.7.2 (2021-10-28) - Pressing Enter while search bar is focused will now redirect to first result of local search ([#121](https://github.com/pawelmalak/flame/issues/121)) - Use search bar shortcuts when it's not focused ([#124](https://github.com/pawelmalak/flame/issues/124)) diff --git a/client/.env b/client/.env index e16ddf3..18bbaa3 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.7.2 \ No newline at end of file +REACT_APP_VERSION=1.7.3 \ No newline at end of file diff --git a/controllers/config/updateCSS.js b/controllers/config/updateCSS.js index 4deea76..4ac476c 100644 --- a/controllers/config/updateCSS.js +++ b/controllers/config/updateCSS.js @@ -1,6 +1,7 @@ const asyncWrapper = require('../../middleware/asyncWrapper'); const File = require('../../utils/File'); const { join } = require('path'); +const fs = require('fs'); // @desc Update custom CSS file // @route PUT /api/config/0/css From 1d70bd132a5f6a496daf407acf144f6bd101e32f Mon Sep 17 00:00:00 2001 From: Ekrem Parlak Date: Mon, 1 Nov 2021 15:13:06 +0100 Subject: [PATCH 055/166] Update Dockerfile for smaller image --- Dockerfile | 8 +++++++- Dockerfile.multiarch | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fed0789..fc402f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14-alpine +FROM node:14-alpine as builder RUN apk update && apk add --no-cache nano curl @@ -18,6 +18,12 @@ RUN mkdir -p ./public ./data \ && mv ./client/build/* ./public \ && rm -rf ./client +FROM node:14-alpine + +COPY --from=builder /app /app + +WORKDIR /app + EXPOSE 5005 ENV NODE_ENV=production diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index 20ff6c2..a03cb4b 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -20,6 +20,12 @@ RUN mkdir -p ./public ./data \ && rm -rf ./client \ && apk del build-dependencies +FROM node:14-alpine + +COPY --from=builder /app /app + +WORKDIR /app + EXPOSE 5005 ENV NODE_ENV=production From b45eecada219c75c8c975b159aa2210c59244846 Mon Sep 17 00:00:00 2001 From: Ekrem Date: Mon, 1 Nov 2021 19:08:30 +0300 Subject: [PATCH 056/166] Update Dockerfile.multiarch --- Dockerfile.multiarch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index a03cb4b..ea1e6ea 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -1,4 +1,4 @@ -FROM node:14-alpine +FROM node:14-alpine as builder RUN apk update && apk add --no-cache nano curl @@ -30,4 +30,4 @@ EXPOSE 5005 ENV NODE_ENV=production -CMD ["node", "server.js"] \ No newline at end of file +CMD ["node", "server.js"] From 4ed29fe276b4282c67415f9788660df2a3a69b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 4 Nov 2021 23:39:35 +0100 Subject: [PATCH 057/166] Split remaining controllers into separate files. Added iOS homescreen icon. Removed additional logging from weather module. --- .gitignore | 1 + CHANGELOG.md | 3 + .../public/icons/apple-touch-icon-114x114.png | Bin 0 -> 9581 bytes .../public/icons/apple-touch-icon-120x120.png | Bin 0 -> 7588 bytes .../public/icons/apple-touch-icon-144x144.png | Bin 0 -> 7315 bytes .../public/icons/apple-touch-icon-152x152.png | Bin 0 -> 11565 bytes .../public/icons/apple-touch-icon-180x180.png | Bin 0 -> 20249 bytes .../public/icons/apple-touch-icon-57x57.png | Bin 0 -> 2579 bytes .../public/icons/apple-touch-icon-72x72.png | Bin 0 -> 3311 bytes .../public/icons/apple-touch-icon-76x76.png | Bin 0 -> 4058 bytes client/public/icons/apple-touch-icon.png | Bin 0 -> 2579 bytes client/public/{ => icons}/favicon.ico | Bin client/public/index.html | 46 ++++- controllers/categories/createCategory.js | 28 +++ controllers/categories/deleteCategory.js | 45 +++++ controllers/categories/getAllCategories.js | 43 +++++ controllers/categories/getSingleCategory.js | 35 ++++ controllers/categories/index.js | 8 + controllers/categories/reorderCategories.js | 22 +++ controllers/categories/updateCategory.js | 30 +++ controllers/category.js | 178 ------------------ controllers/queries/addQuery.js | 21 +++ controllers/queries/deleteQuery.js | 22 +++ controllers/queries/getQueries.js | 17 ++ controllers/queries/index.js | 87 +-------- controllers/queries/updateQuery.js | 32 ++++ controllers/weather.js | 31 --- controllers/weather/getWather.js | 19 ++ controllers/weather/index.js | 4 + controllers/weather/updateWeather.js | 16 ++ routes/category.js | 21 +-- utils/clearWeatherData.js | 21 ++- 32 files changed, 418 insertions(+), 312 deletions(-) create mode 100644 client/public/icons/apple-touch-icon-114x114.png create mode 100644 client/public/icons/apple-touch-icon-120x120.png create mode 100644 client/public/icons/apple-touch-icon-144x144.png create mode 100644 client/public/icons/apple-touch-icon-152x152.png create mode 100644 client/public/icons/apple-touch-icon-180x180.png create mode 100644 client/public/icons/apple-touch-icon-57x57.png create mode 100644 client/public/icons/apple-touch-icon-72x72.png create mode 100644 client/public/icons/apple-touch-icon-76x76.png create mode 100644 client/public/icons/apple-touch-icon.png rename client/public/{ => icons}/favicon.ico (100%) create mode 100644 controllers/categories/createCategory.js create mode 100644 controllers/categories/deleteCategory.js create mode 100644 controllers/categories/getAllCategories.js create mode 100644 controllers/categories/getSingleCategory.js create mode 100644 controllers/categories/index.js create mode 100644 controllers/categories/reorderCategories.js create mode 100644 controllers/categories/updateCategory.js delete mode 100644 controllers/category.js create mode 100644 controllers/queries/addQuery.js create mode 100644 controllers/queries/deleteQuery.js create mode 100644 controllers/queries/getQueries.js create mode 100644 controllers/queries/updateQuery.js delete mode 100644 controllers/weather.js create mode 100644 controllers/weather/getWather.js create mode 100644 controllers/weather/index.js create mode 100644 controllers/weather/updateWeather.js diff --git a/.gitignore b/.gitignore index 98ec862..147804b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules data public +!client/public build.sh \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c870d1..afd7297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.7.4 (TBA) +- Added iOS "Add to homescreen" icon ([#131](https://github.com/pawelmalak/flame/issues/131)) + ### v1.7.3 (2021-10-28) - Fixed bug with custom CSS not updating diff --git a/client/public/icons/apple-touch-icon-114x114.png b/client/public/icons/apple-touch-icon-114x114.png new file mode 100644 index 0000000000000000000000000000000000000000..301cd2527deac021ffaf5dafbc3dc72bbfa28597 GIT binary patch literal 9581 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 Xng6tX727L300000NkvXXu0mjf@N93N literal 0 HcmV?d00001 diff --git a/client/public/icons/apple-touch-icon-120x120.png b/client/public/icons/apple-touch-icon-120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..28ba56d603187f1f698fa1d10bc9e8cb9d2f1bf0 GIT binary patch literal 7588 zcmV;V9b4jwP)pquX!-TyqEz52Zo0N&hQun8N(<{ zRFWtOY9LF*1T_X-G02JMPhzpj=av4@cV;tJgDwKmZtw;12^J`I*ws z@^})T)4yF@@L2h|&3$;dUa@wc#)@MZ0L@)&oE_*w2xQ+6wYHigDceIR{2>aA5PQSS z;t=OW*=?bb#=T+9ajk9aVFjM_1DtEvNIr5-XB{!MHZp@*j0$c&Scv^*&@jMEt$pcd zr2m*HJv@EL0tZlfEE|mZ4e=WQgUmU5$EL?Z-e-p4bU&-@a}gVv2a&!wPDcl4Qy0_% zQTmXwbEluPcMW}ABgq*oqz9$%meOO$44~|K>G%Mr)04@5fov@IjLlv=k-ArlC9$8( z;Gc`w80iZjAu?O1dCmVw2xMj@MToPPVW&SC)y_u0oBPSG!z?6k57-Q& zguBL?drhCmT*bzei8%r&)vcLsBXUH0&S!$Y5%I)90J`EyCDQ9JXvoPHJt63~N$|u`8?br2spjg6kBmQ_l5!kjpDe@Qs79C-WAj3R6C5{bE=x?n^OUMSd>QI}$+vhNw2+6Ky* zmR1U^pQX0VJ#1vGFezPAac$jQHfB02oj|D=W<5jP_zSxtL|emVrZHP{KM=X$OrQ6P zC9Q&7Z0kF+P&;!M8ym68fvBW2A}E`&!qduTKdGn!2i~#>us9s__HJHyTOuU zYY<2s3+Fq@XExWcu`!m2ornn|JSPKh$vsv`Z*|UGJ3Sma=DmisH`bDR?BBcDPr?A5 zd)V0Xt5PuOD1nI@u&ZDFh=LynR{Ql0$!Mzknc!gH1YNqsT5B*Ixh6+?5z(B*MocWi z!*(Z|FqsT)%OF_s?BrBG>Nq$1l7OtS563@NzMUI{?E%Z`9$678GJwKrrp!%j?D|g} zu$EZJb*p#ntg(id9$DlJ9BFWC zT}hxtycV_WL}{|nC={O(=B&EGvUBI`g=E9xaV}ybJ-rCM;z|QGMxgsBJpSaIWTi7htuhwDO`~uq_?zQEpm~ z8j25&UJWY2{OzoCvP=P@Kp@2DBBu*06Oy7OOHtdn3AI=KE}~`2G4|OjZD5347t%{+ z3g$_nkFc?y8|AyKLA6Vvyei>C!_UW}oO9R^69rz7wiIcau(aP|)Zg$%)Ha=kXm|tw zkUYN=lehe*IDKY~yVuRKTs%*-@365EyFvR&C)Z{1PjZsx*iY#&FPfQ@sc(;`0Ou&8 zH13EK0d?*Hi)szEGhc(jb1wibSZD|d0BGF)Q}E8`gvN5&FcM>l)VPw@?I&y;w3gxO zT^$8toiXDX-O`373LldncO=UPF@cE1JEU;hK?y8Ch*qw|&|h4P>T&BFUDXodlehjz z>25`9G6y5GhUGEW?Jum=!3>cO>x$d3I4;cSa04D*q>^>oPRPr4^1Ddzz#Uw3iu!qPL;dVGf#Ntgc$pc=)-6b$-R_Gb0!SE{ z#!?mcMFB#fU$AlZa&6iT+lkEiJdT~6q}&pZ?sa5~WIC*+%Ep_6U^zopJ-cpK5w<>r z;lKI-s;6uS2VcbJ-HNq_K9=tS9$RX|u z>|8*I6)$||r1X${0R(N2iCLFXlWnl|bKi>UhK)g;D_^R2PjN-rfnFXeGMN zm~<2Nzp{`(Vfqx8z6u4LO0-bL})@iphTZT`4Y<2yqsQ=NqIaFjpm=!=YIxj#c zQt@|mgN1#F)pQ}{L=ec&q-g60lgP}^<~^mEe`zMBTVrf8G@bpdbUT7rTCxN~?|QFt zh06XgGomHSyi^2uBq}>}oj8WLFR`7`g&d*_3nCp-f1CWB)CXpN#8IZ-U8;{cKo<&Z z6k+J??*=Ve92CLr{sBO=bQw^o_?ZGh+KvouXG>zym)MxI`hHoO!fqA}J8m&+%hnBg z@#xwo%zcNArA|}&EiaJe7wRk5i6mAM0gB4f30x$CEg_w)5`)OW4%9Jt{)IpkIbl`b z6$<@rKG$l9SFgd;_H9-!Irm>t?r@N$`;*ipjuE2M3jM5Z*?CkTC`R z4uwX^J)hk-aK>v9tvt~4ar2BU%W|eRpI*~dn9)@8Gjz5L>A)W~B^Elm2`oI@srNozN=poKMUuWNs^ zm&7H{ol24PUv0zbh?XwzNau!(FEjWTEly`Kt9v%mi=6KawgIBhJ@)}NHi;nNOb{Jp z5G)gg9pXIY5cKW5?o80SI_yl_h}FQEXXW~}UWBXUPyG+h!PHOw8)!6~Y6P-np0HlBBkH=252@}^K97FEMwTw5AQPlp9tlZcq&Yc?H6;K%cFJ1I#?5j3 z+$@ew;`}nbGrLCyp|)Wos8aECZ`sdz`#^-j^WP3i^R#S;Q~RS?nXT~vIM5re53rH3 zxuY!u66QOdfRtb|0?(U2XBazwJQp5?I=S5ZCwBy{fW zmh_2N9fbP17uXstQKPF9>$O#Do}p?jHoMrE0!ovKodglmnI3I^D4@UddfurJH-$@6 z$k|_6bs(Y@D}7(KK;O2b3*B=K{NCAU+rQ<34NEb2-HLl<3)_~>IC z6GrMj|DGu~HDY$Kk!;zZP9b$u8gi7`^QI<-ZS{lJYufQ;X5aVC&Z#mIS`n>y-ASk% zwnpp66dNCTbnfgdGOR-Wa-Q+U%%I^B3}1RV;+Gz0?@zQ%1WUeL#}mwR6~AW#8)XhZ zrHRt&bKZ8y{dpKNc|>X(1YZ0tvj(fDY{208e}?8SA2cr}(}x70^5Vl3{U=|pyWOP; zgZEl+Of-K1hX3~OQQfey7>n##z>B6=zx)P&)-US%`o6AA_EuJ%UVWcw>Syc||>O!G&!NP23M^CV-WwD=SELrH> z%+Fh}ABHda5NLP={QUE1{_;Vu0SPk@E#4o9qEZG{H?uf|mD{Q(o{Z6d`aI&7trGz^ zIhcga;S3UKyHFsgv^TJkY$j%9yRSO|NB7FW1d(W`B(O%WiN7;FVdy`ADiMa>^v?BfrstS9zfjMHbaF^vx5F0gt1MlPk;^`g`kUVZs#f*0BKsWj><#&h z_BOUJ=)&5DSE2gySC|KwJoacVT}UIN!C5vPbYZ)hFR`fBQ2&#+VfX_d0nJ~qk1ouk z3u*oW3|{aKP!ySmp6uA3PZ!o|L1KS8=)xiq9l{b3s_Re1=oOzvwCsR=bfHTZ5&`vd z&Q-Zo0KmI<<>w}>t*V_k=^(ZPpcY2&! z0M~AjF$VR7rVl%dbRjTs*6TsT!~Tq^)pF^=*6i+_9%QGuw(-C(rk`p&U=>C_dPN8l8vQ#!bQSAq|h@(}g>C=9Z(Br3;g1cVPD?K8oZw&lVnA#-DcgMF$>?;rCtI zvaWxd>B8#CuK>+o5S$&+qQ$v%VX|YJCt*7MX*_s8nvebp6W_RQuh5P0v1>8#x;H?s zSTkF6VHDD(nXc7KX1PAB{~ug`8L|P2zEW-Drdn*9Kp z4?PG1F#fF@ko@|Qj{V{?KK0;vZ$-4~pr9Ws;|BjxZQ`4uP*!5U*a-mZz;AzaBI=2-)B|J9p!M z?*@{jQ1`ksSNV>^!}>cX3C6DaEGEA3bz`j;am%^q4sgrSs)JD5a5~I07uWTkRZhW2 zFW5-=u_F0Nd9$3q&=jlH1g$HkT`qexEnI|X|0RXt#8x@yQdEy$54U?Q186?_2$Cls zZ|lAU_or66x%=$cB*EBK|AMhE|7)w-zYV=uhK9_Jt)Km7Ag*dn>h!+~d%LpoqdRQm z(sq&tG%Jo~!3wzo8nYL-_USCGLjWLJwhX8Zv=1T>q5k@FfJ&_0B5t)?n7ZvJrO1I` z*5s09=3MG!B~46Re=YWW@?*gG*z{#-t~?O&5l36y$pJkdsOMqbGQTUV7130XmV=y> z8_El9=3^@>4{~U(g7Dv(qfkBNw7~U&$^W_;Xg0eFTUHKT4T_>1_XC*t!Ohrp+55pe zcXs4L6;X}V(@xjUsF(U=^KRUrqjHNn#73bM9p(UC2)R^Upt(|rE_A3qpheibhD+|u zBf{YM7lKAdJ&LmV@Iy$Rc)T=mFAbt-#VXK(g*I5=)T<6}jf|l3qC?wK zZ_Uj=$L=dW44#+Y6tEZfX=)i-! zrVHbvj%iE1F&K?IZ^PI%U+S4^L|jGX(ABe3U8plU$j>wnWY<$5$4+JAtH!1x#8<86 zRutq`N<+`CKXfH7`n3<^h+%MvE4M>UAXJW7-~YJ@+&~ zFm?M+kv#VtXx_ZSe62!tleRJr#kKRU>o@2?N7zV4L}Ydz06!~3MWtjzY7ye}`=Hu$ z113OG#MHEl6io+~Dit7(otB2=M1n-`>9IX%-hW@)YZtOx^+D4trtEcP2w0x0EH=u^ zS=XP*i7OXKb<$n*Gy6U}uKD<*g?w(2UM@;f?-a|;CeUa=^~b01Ajy!8%|BPxxSNm{ znw{!CqeyozD0$uB;#eiIIdmbMlpR4EI(nFtFGgO-l4NgaaEs4vHgg}o+a$Ezi|qzW zL`b%81&@z;i&-fB=J=S&v)gmy(PgMcOiltxB36VfcU^c~UwLfXW~8=6!d)Xt_+@z( z8oRM;Alb1E$&T%amMra_@+;80WwFi8_W~0WPSaC1e!PAjss2m2R3>L`Rpahm;3QG! zE_Ge~W;(*oI9m)=`>>WxO{bw(FDQLn7&_zRmZB%pEU zt)PS17lzL0x&gcSTfnJ800%8En~5Q_^6k3 zU*MJ`X#V& zbxj%ap4~_ud-R1*7iMcfO#b+muBr`ssZ^N!?!ROFnlE)uy%C|d@k|pM?S?LFKJ-9Z zG5G?g3yqQU{r><@P8QC*BFe8_WJ|BdFp2SNzk)rV{&>nEcSgNM^A})X(^);yg;PKI z9~&o_LAtQDb|F)qj1#7r@NKgoy;u~>3VxCcx1^^aZ|%b5v0tNc&)ul5JJI(S+xWh& zWt@lQBti3m%^3Ug=g_$4Zq=NC)RF=4^qGt&)Zcg>XyJZ}F6<=uf~iSN{`eNLYEAp= zhP=&gu(6qS&I%urF4RxgrmqIpU-sN`(@dIVRID`v#=mhL;^Woi0aDX>$)q< zb#l)xcE;&K`ML44yU^HtFRCv&reo@@C@k6Bz7^wFUxj4LQx3i}>aDWoNDRIA{ie>S zlOH3xlO`r^yxw{$T~K~=i!PMMHl{Fs%~dUz3BC2)kjKP_tj5Scd$F7el3*p<KwHnHc*%h7!5iMEuVZVSv>9`}Hy9j`vQ%=8cz5^`A#v+S$v ze8ilyT%1@LeBOMFe(DQ|mLH%VR@oQr8e1-w_w2^xk8Z)#55JGb{r9zMjI)?beI(}rd)qAUh z>xSGT7n?k@1v}sUr$}~e4>n?zt-JG{k=MAj3v6r-W!4L|iUl9Zge+N$1Vc-@OT4DK zH(kfptVQpxaVu`EdEY(Q^Z8F>=%R~lU2(}5>jw98XBH8n(NRRBqrr2AW^5>%GS6)@ z=W8^u`%_nd#mm)8V>>1vlQLZ04!yl#XPhq7`&fzb$(z27XyJaSpLc;}O!g98*e&H( z20Q-MFQSo!L#KiAn^Sb5)Zdb2%F0s5zjigozy5EAH{Ba_VK0mRYwZh| z2FkA&Y)k>CmRw7%&BBf(*44UEh}2&oAO;6XVxijbz(a z488L`CRu*E7^Sk<(&yy_6W4zYV_*FOxY?X`%C8qJZGZK8_I)n@WA65xrS7;4w!}|- z*cuGK?@~mo4njZv@a|n0``o86dGk%G?NqlzXS%T^{ooFKo;-WK6O2n#jgDaOEf=D8 z#%n=N>xWKeTp3He?6J&@#(no-&p&?x$*&&vT^rW!j3H+b@FDji)_K9HC}mb1)}@H0 z(jHsB7l+a#N+~MZ7$X%p%^$rtYZsMB}V+R<1w)5EX2pGRV@*VUT`OD5HkbgV`$vJ8Iw2P z)Z%ofbM=A+(nTfLorLPz6Hz&QO{)&K5_i0AA#(yW z8c3emg62d2hpD^oK;zC^kvy}d-Iy;WW@iJdU$8sfB|$;#Eh5ciJt3Q|V3{c6ACt5$ zNqA3L3%SIwGK(u&&R1Q9*Rdl^{8KQ-YTAg)SGhSV2BC6~d3anQ_Wlv$JzBvbs41wi| zaWtQN9L*;mw`DV-00H}&$}=3>`{+W1*3GqBmBB~o89URo-E3iJycwx9<=GYYa`^{9Ee<&^HR#plfn@6Sc7Bck)xRxjXz zb3&(-hb>Db28f2z>6&e@WvDkBfa{#L>nCg+ieP$lVJhxSRtM!&X(`dj!hg#Nf#y}V zfCI{fgN9l+SQ{I$qfy3n^>}x?1v{kcajheAn)vB#hN=1zlTkK8k+Pq_rA~vO{ zS+l79@%sJ+-yfd$dCooOp7XfRbMNCmo{t-;_3|lz21G+dLGy5mqx8|W&Vo%$t0b!xyqUCF1_zv}R zWfE%Q7qE`toll`8Pi@lN*O$cTHhd5vxavYukJos2x3n<3&npk@5KvQeteAP~4W zai$?;%0OjE-++lGtl_o);Y5eA?7zYHI4WeZ6~ixx07okGvHZ zY48m_V>9}Ob|7f&dB5vU%S#;V#c4POod$)B1A32%yS-Z>;TUtV6h?Y~ zMPJv@uEcae#NeqUZn^S-=0|*6oyD%}ex2Fm^bX&&IF*)2bAuYbqgK7C;LuE8j+(Ue zj%Mo{iEL2?+HR)YH)}aG5t2F)K02{ZvGz_7Wz_zmDWmTV=GUW-x!Ht$_d_^Erxl6> zz?y2SwSkiC96595Q}rna4g!g?uhvs!E%9^$*RM zQjNm0^o&*F0WRvF+PKwlfd(4)nP!i_sQvqWSGtmym_CkGk-SQrRkXe$vQvFNJfh}b zH~Kx>l-57xA9VEYnvPCvf*uUC3yBum~rGO@%m01|w~bP8mUXmCq$Re)md5EUaIUsEfi zJ%Tx784}N{+R+h?9VHXqd=pRrGp4e5c>iwTNijYN{aOA3aur-nJky&T$rImV&l4Or z8VQ=fJc*HY$6DAf%FE@x_td{rDG9H8`_esF^8myw{nXX0k&2ZSUM{-6V;>4DWgxgo z)7rbge(Vtb*xKf|JQ;1vjCYdm{eFK#wA<{3N{5Gs221);xIMDM{GgbLLpr??TTq;0ra$fhvv2!@;w(`FGbj|P&kb;bf={V^|85>Rfwi;)w zg773LfAxZOtdQX5hnQ3cGzRm+x=ZeV8IkeRW}*fzQ(~ML91eYn(8H=C)Q)LX5+C|t z`*5J5n_{I*@$x?L{(>%5)Nuy>BnBOp1NJt2;}sfK`%Bar@=EDOOu%Y#ETD}!R~wKT z|9zubZ3t$h&dK-Q$6v>4Kq1#4o8!0k%j$PSz&@F*{il#%jm2mo)rq-@@yac=D>l2` z2JQr3oh4DLfmJ^wF0A{bg9@B~RPOGFPv9;tkg9Yi0{c;m?!|;VU{gQ8Ys5G&Mn0zs zXP}DE=ojx45P7>1PNmOWJINJE;k7vB4scI?Z6eW~^S|DQ_3Uyr;JojF_cZGro$Y{* zJC)MiWmLG{_#yn!V4zz5ND;g^F0*1dO*1RvZiCttxwZL?+Ul@0{Yg|Jk$A+DgEyus z*_UF?>3msLq-i6d5U7F`&xdFH?!YQ#-WWibdMf_W<8lYYN;TjYHv~R;N*X{MidlFocDUC_WqF{3VKBDUX}VYsULkv)^O1S4Sor>!C|++gm!s-4 z-nfC~jQ)yL%vFk=BYQ-TsY6;M{BSQQHME-<3Ie_1^dH;oHNO-c9W(;9Qi)J#1)SLL zk6trF^EpIEKXxdsK0aruwreHdj}SmH^@T9LVh@5D68_)nDDqFEK{Ix5%8c1!V9fz1 z7vy>wzSRY9tZ0CV>wj>c)s_!SEQZq2#`Gz12?K6si!Q6ze8?Sj460%-_l$z$c}g`g8=BLS~m+2?6APrFQ4FuXE{v z+hCI$J3B?}=g?X!#uhT4Vh2QfbaWnme<~@Cj&MmL$SpK5RjO|~yFr-@aF}mT=<3q? z1V0h2=xR@#BKn|Qy4;<3;Za1D@LcIf`c8^9OZu3(eX+eC2x^QLetKi~gnpGaPAw;Y z+xC*Q{R7(gRUi$}p8`5IIJ41q2pS!n$)1qwcY}bH8~VsN$s^^-f#0N=gKL= zRYDanTPqh^l&!45YaT2vlhdJ{DEVQ(n|YiH@i*phg(GLbpRBBs>sL#lzf_b1(^N3t zyllA~nBA+_{2->q^3l2Q``DBJ72^8`r6|aVU{X||hj)beedp)1Ga@@50d-%j2n$AU z4R3ul5wQ>ORHkLb`B8Z?=;_2`v$`SN!Om*&1KImOG=mR6jII;JAm0qC$s&^gj@Qak zb7oqXVh>(x$aCsya^+B`dW_btDTRZYMWq_b#wHly#eFSj&XxM?cYoSG4w(`)3-;>f zzfvSbw-jWkg&$ytGrIO;a2^HI>pp3qm-|NQFxWQ7-ZF1=blL@tuS96TDi&k4 z2Kwn(Y~T4jm2nqL1%O6W@Al?ZbeJ^Q143%sTYLedrzhTh)px`VKZ`LiL3IW(c0vjy zFy|(@C)Hn+j1E9AT3^b?Xw5aN|6+j&17s$ih9cmpQ6_axik;!uCQK!9jFiulYP)7> z=AOa`U0P`I4Z2zDhG=JHasaikSAn#ycEx@HWy|J$6kf}1f<&P(n|9Ve+~e!6C}1+N z%kK6b>uHptQX0>@aM2x?M)ch0l;Yl7M!^gm2=7w}P1CX_Ttno3z8IS{xn<2`-SweC z;<#n66S1r;nP7=5YMMWUYrhyr6#j*^rtJ4ONWz9_iCU?11 z$EK~5a)zFgPZvGVNZ>)~SqT}FIId~OQRFxa;6Dydtt@IH6kCHYFBHhCozBXje(^Hl zS@YIYFYGbNGd?7})gp_OkBDLs;e!zxy-qTGxyQ+PBg&p%+Mw!4{m)cwz`MBe|bW~ zSBN9EE9Oe{ac+GD^M2XP(080j=I|YRJ0-!_%sBV28!!^;9#SBV+en-zBlJ3`nWXo$ zfTOBJ5kkE>+{Q4YI~;!WYZ3*Jqn{vyI#EFJ&ODxD_8V=N{G-uEw**9ip;GEi58^z$ zegH-7!>&Xa2fZYwRV6zYZ}{}6`DcR zmD9owsMMJh(k%lLb(A=N+y*-P^n_+>;1+f~hxVXJ@U^DdrVU(iEwU%r0e%k}W%Lc` z8>JuC-GqHX(O7>YMA5qc%2^dj1DvA*#`R_0R)*s@yI{}w7VQXuEE4AI*JV>uMid!o z!4*7+t=+}E`^6X6BSIc2L>j5Mn?^eyDzxTX|52VDI@WGsiJW%8BqLSGL@zl02Av=6 zVv+{(II7qFx)5h4SUVF&Z$G4dDlxBI67il@FL0o<98>$P0Y!E*eVsU3BOdbv;;6#^JLz$g#9M2OM0lo1l9L$-Z>F=VnnXW zfA6reA_}gWb5%NiJQ8jYTmzCIVGy@%Np#Y{Eydz)qeoKb?d3}F>dZR(f(iFfbwk-+S*6@AHhtx|BA3T=7u&|M`8oQ>O zZ7u#|uHJ=lu&RV&2b%yc0}V26BTs%I>w7fywvG03isX*02NWUne+;TMvks@bSQh1Qy)aP`U(-vpp&7b#9vu2e z2y!Op*o&G7eVZctzUZ{>RN>Qu#DdJy@9EG?a-!E|{U)sc zCTjn-c{dzjRwiJbh0E32|FOEqV!y%Zgc~Yo;!R#Ws3SMm*ZYcYi?F3@uJnlpC0G@-LoE2v>ftYoq>$AB z|JCFT?{vG(DH7J>fP0TdKZgD8&l6GC`8i0A$foFu^>!y3gGNw&aRNcZmR-lOpcF~KsAKUe@0dx^#t)z^E! zYbQrPrAAog=HF__hHQRiAW36=3tO-C@sL1JLZD~nJsj+UVZ4Aez_;@EUP7$L!-POT z*@(9HLJ$A^_AL7lWBFTfx~F_R`St2G%Y1n{C_BlTj8pXmrl^U4obK&^OCCC>$~Z;1 zJ+tuFWVRJ@G%1p;eGQ4aSw&ASU}ALS`zUQq{WAYE;a>|LkR}-Y=}` zNQ4!y-yPDJ5Tk;SgezVL-2eWlRIEg|HL&R880qQc&zI>Vq0U`qf#FH&{hvB|wFF{a zIzmdGgl!G8Ig3L4heQ|k=y+x`-#Laq`Kc6l0PM7}jq>Hwo3UENKD8Lr@=l;QYfk%F zOgZ}^Cb^9vV6R1r7b4DEe>7Hyn2Q|mWvcfpOZv3+G(@g^i{Nx~W`D>0)K{wF3SLvj z8|I8qxxL8;mRhk|c0scxjK*8V6U5|dtCSNipZw)VPiVRsQ1kW0;%BW3lcRsvlmY^3 z(Q%+ErQM-*$Wgv-3yaV{K@tlg^AwbY(V6$hnREGKs&f=$k{DYcrfCf&mjM+IL0qLwT(Eifo?-y#{#hWKh4;7pB55c_lD z$iQON)`1lw;JHKF1ne0X6||7E!Or1U|J-@MEKn$SNU2xfLgd&dHtiVe`K`u16hWkz zP2@O}z5Aj~h859N4+mS-zXzvi@F)$Yf@!I@Up>Zx96KQO?)tT$VzsD%CyIDPPBkeW5j8Eq`0iUYO#8}!z=gQJ>&^o2nyz(F11=iu?{s3Lf#@#Gc792w*NY2KVY=47WJ z-@V! zm9IeSYkzh{CZKLOWd#nzbH$vaSu<~fx^Wx}N<`+DKBELRaITE+`X^gC+NsXN^+c%( zQ)wp0EcV+}Hbh6d3_0N~sP#3;r7J6l$o$tsG>>%0;BqunJcmouzZJAwfIQ!cgU0=5k8GBH5hpGyR zdI*2j4!9m+3ki>>fEHs9Nl(21NWq0G+UUSj?nhw`bPYC|vP`*TqbqL|hcgxmUFK#P zky1s9IXCY&ufKP2burNZ8+DWp>QY4ykGI)K@S884=CLR(og9||2}}D3)K`YH?RGPdN!$` zFLJ5~dT}k$d}P2YK=mRRDeN4vs1|qi0BE?0@@-g2WdH93;ayPH+(fCOv zNCNo}GS6k{l1NDbLF7!$m}5skI)PI#CI4{xo?ga3CNam!h@}kQLmZKXB|yBNejYv6 z;+=ii3+}i$ksuCcqq9?0_16%`AKc!}N74D}OVvl-Jla@NP14V7eF= z)|d)LXy}&Z_zaxd#2L+Ta5ADS+FOcG_h{)bO$r)Bl2|w^sljW~=ZiFQ($MJf{n5aL z-AnbR8tJ0^<}*$`_Qm;k6xQG)nLB1TBb9P5t%T!Tm>Z0qmUgy(ZKc^R#F;eJG`<}% zb$hqF!+Tr5>=wI+BY2iI9wQH9L6#FKqEEHuEH!FY6^0)=SX-(2nP*4dt34HcH#fw4 zy}ERXy>X{#9ZWQ*-ru zto1lvj1#$4`O3e`W9Q`4Mv*9hG32y$i^9P%)^T%47jh~YA9CBYRlCaEWgrVYepPSX zX^slc>MI1F#WA>G15Zq}|E;d|I!%-H+nPM@Owqo~yg{60|70nEH7khF$jM~KvG-=? zi;Te068Sgf3I(oQ{~mN7M#5XyetWPq{2u6i_}%Qg%!<^@64>x)pdL7gc}>AZ?N;u! z&wQIRC4Xa-HREaV*gSIZ98hV%PQ3+2`O4n+nzh^*|4M}8vWOYd9+ciF#fxX{Z^9(= zxNSq$gOlXdb@$=eGKqZcZ?dA^>=Z8l3=EI%hrbMbC~lvq7U+8u^e8L3n6=o3BgVnU znoPyuJqe+Jo&K3QLq;ep)`$mgT2&*hgIf(87WgjW(N((?7dO3$woMY^`O!PrUp;U8 zHEFF5^ch7avuHCgyZ=Xu<*H&Y>wIG6mSKx*2IB4PJrg31uhEG->|Of?tyIx5__5V@ z`q0V#dNH&{IF&Q{Z`6fv`MTHV4*SMAc9grnPYd8GW-JLC@(-(?ZGGm1$_ORpkj}5}m z=vwK*NW;1`#CR=7Tnr5}^=Y#9tyd=Hc18HY?wzw#ntnwemU8mb>ffi#BdLjQl}>ef zV5AnS<*mQ)LDP)-Vzg_H_Q702&cHew<=VyGt<6+UidSo*0Nx^grE2-_S>E(DI(V*g zw|x!#bTh-eK{DI%zO@q$y;7we9dPgKFt}6Uv z9F0zK4vG1*uSmF`(bXGcuCn$+$dsD8;0{tR&aXK_(epRtCM32YhPQ?9gMK?+UqzXn zUG3lmoZ~Y$w6siWN=qG&%CX|m7vrS4(_7EsMMrC_n&T&_0v5aNi27h5?i4P%kCC5# zP|=>mIgXw#ezE`uIB9*>;`tUeKJn}d?vRGi6~_H7krjP65Y*-CE-*nOFh-~KZ){T^ z@JD{dhprrkxMO)fH}QR?u*djZNUSB%*U zp4y@cSEq`qz0V$dLSO)Wl)_nD^LjwDB;xJtd>&}l|6ENdA}=`#;Y{?URMe&L!1f>| zdh&Z8d`wKr!AGO;AXP_Lo_R+1rMJn@P&#GxpL~ASl|uJbD{Hbu9;IdM4S!O7NGc{?X-8V8Aa)?%!7wXdX#r7I2B9blrL0gB)_gyQX_{roikRv*-Y zPTaTOfEy_$Inj-dlUy>k(7wW4Mrk@N{_LhJRlQ9Adx=7++JSFr{}C3>h4I$i*eR?v zXx0<2HUv*577J6c|5~hV%H+EeuP>Wx=*R_{!CJQQUyvAhO0*72;q3eKx6!gH&a3 z8xpfBkkx2(-HE>2p$D6O)zUBjH!XiVNn#*8t?P~@1eX4qTkZi|B3GJ$VcC?I0@_3@ z84WkTHs1^@s67{VYS00009a7bBm000ie z000ie0hKEb8vp-)9-WPV8g)^ zWxo}jD~cDsN&uL$FTDa-1OjK*it=le;rri7AhP;QehzXE;@ZYa^AGg?%|7OiAy5vM zO*Ez+6wot#M6uhIV*+5(Mvoc!Y+?e)e3Zyo%lvp`$BGArv-c?fiejkQ;8p~Sn83vD zyOH|lIYM^fNxZ{w-#!20YaJ`~lY`B}?mx)P?rq|1o>IhOu-H#LaZ|Dn=ZC^Sd%XwPy&;V2TE|M{Dwtc3+`#3A zit_)6GGpj19GEhT1vj0@8QL5oe}$BlV(~TH%_aye)~~w-+@Cj}9XXa8d-nNZWA(;P z;=B6s+Uo#s>)plh$L;-X=8(9VuJiQwKu~tKJn@ z6fCI*i!y&=C9v#wiChQ!ufh?&&RWMx)TChcHj-mOgI{;mF)aNU_CMQ|m;HOeQpNJd z>SZ}U9$Xx}L}M9ozF}d>;WAgmUz}Ocs^iu$R;sS3n2Cy<$Sh57i413X?qBJd?2pN@ z+~?I&ECLJjl(;bzj7!bm*d&qGa!6P(V`gdnQ53LvKa(Pdu4Sx*g?HjDjQ6*_Are7o z=ID!0y4T#R?2)}c#?EJiV$39_KbNwE7K^=yC$UI;hn}GtK9>Kk=4p55VD)_zts}0{ zzJQfd9Bbo$CLe9&@A@J;ji~M_Xk5+%=AsDsyE>MQ`#bvq+v?r7*^K+=#S-YLZz(Gshx7 z$>q5z;MDYD=9fJ-6hsy%tOQp6y+}_;KY7_l^+cMe7B_^YC$WaHlF(seHF0txR?<`u zwk)3%1u=oWj{$FHmo85aNULeEY<$6_ViOjR)QVsU5DsIkzzNQyW4E5zAbbsDrOt^n z=8g%<(!??^y;;s+`8kB1b2Q^b+O)DJT>L6dHWCdng6Yi}*{dFg;bGYTCBMMVD{w?H zi_o3bL(cmMR?2ch1-ulOhp7dYE1)?j95EM}5VGH_GG$m4`h=!?NLVu3WR{aNehJt- zNL&!Pu0^cWl4ROc1~yaiBSd7@XLd^wQzlBc#4^X8;|hLoU%*Oont}>gTBDIL%OY2- zjS;s{CPtYf1Rqy%>e^Ndhk!^=7%Fj02gM>48HY>=5s6faeg1f#!AkALb_rdMC?XYD8H>CL^LG@%Fxf zl>&O1^2DcKJ*@fkx+zkmt7rawvLGkpETPKAw~@!Xq`LVxXF2omILsxovgjdlx#Rff?bO6U`471 zm7C}AxY+B%!t#MON9>>ej5;^7)GeBLZS7*E@=7}5j0BaC|6j~3Qt1_nq=85fbEJJU zHhC^-q?FAYB$qhjlro1L)=FTB@4!KrOwNrkk2siPzVO<` zN*q~A6bVJWzhc8tS)4)9l)GLhZ4{FE`^eR~VVJ8mN9qZ(7eHb;Lf7YEhmJ)e5+yiZ z4Z?MR3Fjr%9Z-mC7%SCmf|z8&LspVvE|3^UB*pe;DZgdO4#03J@=Fkj#CwTTm1D^V zfyLwc|}B zd0mu@34~RNl}eCumQmCb7F~Fb%qo>~6a^-%bAVcp7R!y2#^PwYQ2Be&#QC6D)*`oB zXkBm-S}%Jg8rzNs8VwMD+0T9&SXhW;0g9fbc)eykjkbEQQUv+7au%D(W)!1$Mhnt{ zyPXrC?32gJ+C*jrjDk_}2eOFQk}*Ta%3f?X(R{&+F>%G!NRB=RlqBvc?(W`&_V;hh z3YDcor9xs7@H-}>wF%ecNKZe@O)Os>&sh5D|3mi|PxuP);QWFSkZRyuIZPB3 z46atJl#NtJiuSc46gsR|AMl0bQWPZn8W4r$=3d@Gu5@3GP9^YTPnrmq$%vA2>EIBt zsMSL2@+&ZY#Z^eArq#rf>~{Ny-^;!S>$DlG2+5yO@lE7Q16eZLs}m~?lE~ydV;JM3XdEpYwbayC}|dtue<-%EaN335kQ{tjJ$o23DG) z^{Urm;;Pq!TCIX8UWezM2kr;v=biOZHBI8dvuz3(nm(}_tJR8?Ml+e}z@X&1m?Mgi zsv(_0QEYXS5hf-jksD%Hgxkx(8lS-AwQs@L#g~8@jmj|IS+E_C`lLQ-^16&vQz{E6^wzcE5YJ&85v82Xkk7@Qj-KF z0Mx51sRyE{qYuZJxNNy*D*j9**2Dy+-gO7^7x-5mMCPO6$E2_!(mv)>j!fG}#^^QM6 z^Stw;(R0znQh~|LOmIS(3@Ca;iKsLywOX-~$g^!%7uClnFDZ*JkG^4u%RB0_X%|rv za}O&gb*0(OXaLS^uB`OQ&!bjYi6xdx2G_?*5=^}Q_t1RNg+g^fg%c@8;Sdqf?Wo9E zKL`w%b|E&x=E&N;nz2$XV`O0-Jtss)(xM1bDd{nh7?V#0vFmfF^+HPMm-k7v?Z=p9 ze?*s2P|O2IQe7M-KB8Czv@W?Etylh5;S#8$D=J(v(P-!gPAwU-MJR`>g78oMvc0;o zQc3OG4#6xE!?BQ`g+Ie=(uhv0ZF+RMiY3y^sL+DMOOeJ?gxkutdgEZYA-Oup?8fr-hGm9H_m?6JwFgUpld z6(k(gV47uCHU)PLXJ}PpB@hkfRK8g>eO3In=)ubH(S%0fqifM>!%Fo{3G-0Zi*86U zP(*@ejfs>@BPi4N-;jzr?U5N=P?kgr-(u0m{V?&mH-ge;X=E5n36&{98%G}tj8A~( zW+SmZ#e1PrLqgY0EHRO#FVN$?`Cs50|mni7w{ z=1L@+w+O0&1Ls~j?*$gk9+w<=Ad-U*(VSl$U#{Z&tyV0{CIyDCj-m zETz_Ck$9bpKAYz(WM0o=iQ|b)pCB&AYHZ$uv6o&N_)=ECR>!%wei_DENVlJgFi%jz zQVOc3{H&`FtJj;twLT=@n}VswgJpkV`%X}~$zZWlh~G8D{F^UhEO28xN~k0zNn@zS zMe=x4W9Bf3F#g+Dfi`T2lBhAq-SS;lU}%qNo_!vWBt8OU}ofakXNl zl1Os`GBRD3oesz}Y%{Z%Mb)=duqtAJeXXHw79@iNCe=o8I2>3 zLi2?$4wk&w5@;t_EC67m+y#Ji`>99{ITW48cW8f2>^8=3&mZzpnu)ZngS#5B63U5I zX+bgu-$rY_r6}(}!+{e-EcOQCj8b61?usViy}*xLgEP4o4hi0kj8%1Y0C{7yZ1Ut- zF7gqr*-7v@txGNkO-)y_@^;b`1B z!%FlMDE_i0HvL6qM=9vgUT@T(f&-zmCvo#-A7>)J#f#tYfL#7k2MNMoRKAz?4|AR@ z$D;iYz}N*ZDI`iu_uwhlMEVYPEcdCG!RIo5kv9sKzjo$n^X+txBwfWS40Z z?aSBmNRCwo-IsfDjGrZ~C(QZ^~+NZ^Ww-Yu}MfH&e7Oycl`|*7#CatKzxp zSR%Au{X1xX@4Mi=d(80m9m#Y8c&EEsGMyKalI{i~|0NOT$Am_r`3S40BqEs>MI}Tf zGG~~+{?^tJ(AaVm8Yi4oNUB;3#HFfTuC!)7()Ad$E_+QCGZ_RYMbcy4~spOwqG?2(NWlzU*{?fk;`iTo3iO_^!6M-KUZNT37Th}TgqS6b?WVL zMW1m|=hQSN-|=oF8#c!B8j9SeSY^B+zX7|i`DkM$-YUf+s7$#)o<5LU?M4K)LIX`= z=@R)tBDGsvL0>xrJ@ttM7P**_bBY2~IPH^z4@KkHZI#Y>nS);x3>3X5Ct73cahQ1X zJAtuQs8mqCOn40#B=WZf&6P(OD7+vqoexpH zWprzK9(k-JluYXm(XmM*%h_bt=BQ*^ZBI#B2EszzDEfL@Z^}^<)-}^q#U&&r#ZW)t(>ctYQn*@8wG`pox=Qn+u146R@ z0hoHv`@y?+gBRN(67^fgFo4Ff$5k$CcCz)$joHZbmQZo^UeSH~qk4scy&&N)qRP)yXsV>+r`cp-n4k-maIfnttyGkZjyf9pl`H zTxnvewOophy`tA$OZ(uA2-d2UOk;8iQ}6yWG*5jVmhQL>{M0WBCDU}^L7=Ir!lkZE z7QGzFbnNHY=bNWL9~(aWaWqak#kWRUgOcf_XUVu=@5Ibv$&`=Ua+q~*b!P$LWH^-J z=V>Q0sUE+p2!nE;jl|+eI&DSG6qCRI4y31@1!hM3rtf>nG|7Z*Zvc(MHv>YoTDJW4 z6D<~fzARF;??zqjA)@jLfa_}KNjeLo7UdUlBLo!`wIaQyZ-hhcWy&a7cw`1;a{~G-C&SHLf zT{4Xol}xA4er~UoQWi1t%nZ7ZJyMX|&L-}Ljltw9vP{E}OjlXKXb{k7VC=$+vEe^` z6pfQkDNSgsOQs_~4?{AYY}km&x4a7&pKv#+`^4km`Gr!+G;Q3k(up6cWV(b`D@&;m z5gN}r7SkX2FvebS8IUA}$-46F$GT*?Z^<->Fn;ChkR0_ab)&kEKL&J`)RJf=(|M(J z%n4r!$#k8FG&U9|^O>5)g`uUiO>8GS%I)8{F+GeUg<- zQ?nVBHLs*(dQdDR3C1qI1j)gNV&+5d&vpx0mrSpWWI9cIDVFi^@Ts5KTPc~gPI{H2 zKMcuqmBo`|DZ@%nI}=maT@N~7)4F7OMI_T8G*5p%($mk1FGaJn)spE>r|-ya7?SC_ z%V`x>y8Tp4|H)qp+fOlSAI=INKZct6W6>E<$DpXOQxMOSSFcH4%>|8 z^Uf&rPXJ^~29-*t`I)B?7BdVeneKMczU3w?-TGs+Z~kHFXX~>6b;dzNXubS0q-UO8 z<6f;xrpx%@Udh+wWclC^-wdQ_sbspd<58f!RB1{2QBd&SJ?K1eAJFMw{?mU4o}H;> zt;%??I;pEXs-Aq~TR{hI!n$M{%alwLq4}bJQ<*$Y4mqsnhm}aCyN~Yxw-;+U)U_qk zyPg5h&VU){+OSSy44|>)NQ}Ss-vX3`>Em_DbhTtUIr3;Uwj5QO zk|83{lLdTw+e-ToOQUr>NECqLa~(u@ab>sJlgz&o_}@;^dM>L>Y_Dp7qV8 zHbUQRBN0zZYLXSyqbJHZBzbe1XP)Emne}pS)r31b&rluNjrT3wDyc8Ye*VzOq*WbQ{^hcQg7#JcVI)@^L0 z*bLuH#2=T12gE*_i=+)yfd_lmmm}Nu()0$TTaT~I+qrbgi6?^&KGcj0K5`_WbH{DK zQo9nXt}~9YmfMR`_Ay<$^6W8!Iok`=a9$ z7(3^MBAIS|2}Wnfqv$;JU^P~WtVS)D8#ekv2eD6Lk01LE_Ws8|$>cEO7bJ2NY zSjmw`XB~sg5+B*WQT?3VXJ5<%lL{OTRzmVZ<^BuZzFWvM5 z?0w(6(S3YJZ5dCM&sF%a5n=q5uLg~^v`*f(4UbTczWtTi`Ofyl9RMrwQGrO;$h423 zD13^*ZpA`!@`~Th9*t6(BhKM08WEBsjshh~U#XPFvD?r*<1A3H9fZek-}D2Z)2W>f zDkVdwr$OW6B6`v(DxkM4?|siZ(Yg2T+Nq&BDXQ4td4mt#lqo3s}4eN{x1@~Uw|3R^m-ad<2CATA4 zS!BxdkeFQ|t?D9>JL%c`s;-V?fuvyav)38=X)%wJEBI{|35#LjBs>JqDZP5{8%9A-cAm+0Jnn+X5Q>o3d36+8sZ zCK_9h&#Jz--ndBUzR33zVe|7KScxx`CC}Qj*r=0P=(`M$r5a%9^pJ0op$h~YPsOCh z%FMvnIFP30>^;h5&dsyV#rUhQ&|3gA1C11|mtNYpWI8tCm>J38M)FwiOgl zIGH5*%!faK?#`$Bs$wcCM6nh;J#o9}BU67|Zu>oD6NIXH0IWnZuXb%Ev16H4TDgcR zQa8uO%Tfg(Oc4nPIo~EP*D5OsEBhBj7{BTb7=PUx(Ku`~s5Oqp5l3R;+P9+l;@{{G zt1Oq8rf8hF-A}qaauOx<-h=QDYv+-NF!!myQBv-h%(`yC zK5XOuAhEG(zvBx3VqmPKsLN^4{6a1nDCqiZ_Y`6T`AOH79AWs)v$H_ET}$z~T(RIV zv690M2a-l`%|ePhd}-;Ho6z0$Okd+FlUxG;CMJMZE3(YjAD5!37z1M^BsKyi)%QF` zq1l);LXu^sR5ST9&5&EI$WHgWo~vfIM%< zd-kA{sXXc|kF{$tr>1q=hCasy(fyW)2gXWlQ3qgmRfs4ZT};XqL@VnZ(!lXe<@5Kt z9dvi>s8%ze{?2DBjRlneW0&ApS5AIbxE1YNe_X2uvutt&0ZFSB6ovFRE<=_Mh?Q7} zoJgly*hB{^reun!7{zh1o)~Ip4`_XEw~Nlh50s`<3N>0KDW8?VVrFz__5$6GS^mig zGKG_@?+7{%-dCv!E9>|Z{a&38Y%xZ^iblED44cX5HTbTy zEb?UJ8g5pNN_q!&AA7WN$tX)q$0&wjV#(jT58e-SI!@{7rN!ibcF~PiCornxKvKOO<>q2+CMSALWN3WSw_h(AuS^@2DRO!59`O8} z5DqSHT+;iI5~$^1jhtkf_w05Ws@QU|Y?P+6=)>^z z&a9knT{2xLnMSvZ_P>6=7FE!X%z3C-bF-NL+-KCr5zCPiH?|!goxuG_rsbc`lTQHi z^Xro7GRbr=eE*Rv(x@wGsdCtdiM8;RFQIe)Jp;*!snI}s@~M4sab*^ah(Y^CO#lE4 z(@8`@RM5Hi=cXj^*Dsk)g{(}it(Fqe1=@Nxk>SMr@5Pd7J3zUWf>(Z>8ld$eIIrvaCwr(pFKK7lD>~_()=PtuG^gpg> z&zk|TMoTiC|LJzn{@3pXJ5DTD=G>3`x(uuH*dv(v;QN5NS-&_eQ%>AG??s?&&!Qp= zR#!3&0GORc=k7ZP9#`xxtsTj<__O$(8^B9Th4PhtWzPM`uVcq_?!UKJDfGnSgUg90 zCegb1(pp?wUCH#){r8~z^izY4D{|bLl1$5)-MQy3bnd#NG<8!~=G?FRT48lMSp3cn z*z@kU_lVU&<-`ESE_ex&qmHia6V~y|+TXtsSXvr6$#lvT_HxL(OcTi`+-rWqi;+pD zquMg*WJw*FNmD|!<75EsHWvQxKclhz6v$4-RZC#ZI@w873yT@uM<2%g=RSkQ8~#t$ zXbwRKW_Qh^9hikh%$@s92V?w-t6XJx-R{?A5hO1xxT2Efj>~n17M~vmR_eED^r}-Y z9iyyK54j!H*qqAgssdk4Gxr3wW1lAQT&PQm;%I;O26V5u3XQ`y*SZO1+*T3)6?|r4 z9-aH|!@}48H`?F6p+{ay$5ljBlCF_iStl#DjzS~FAX>jP)zRGgM}}B z9+TI;wbBPE+uWeCkT(0b`{d)8`SU;Sh5N4mer{NOYj?W?Z%*%!yYQ^_D9k{TVEnhQ zMDzR$0}Vh?XX>h`mx#cf4i>)r1u%D)H7>d8$kgDluo705By224Y8PCHCl8`AR4kgH zzzvi(P?sioQcP^80A|MGH@=EaR)Sx*LrtAs(PCK1A%|h&^>0FQ(7`ZU%((vRs2S#moP8?z;!=TW(&~xavu!2g4dE$u#LrReMj(?%kOG?0*wI zss@ueS4pPR)6T^72S0}9nP-a@29Xl^nsVY~(?OVe*L9e<=J#CBv#N2`{VwQsvGB#u z0t@p)l{v3j$#lUwi^UthhNWA7R9kpgmwYW!Cp0#1!StX01t#D2he(dt5~pygG(lypPwE|MI*6&pVEF(mu#A4Gy>HzejD7dgu@tnRL7u=vfdWAPjR z+-v;XUNB!vj6G=}*>n(^=bewSi!VXrn5~5gcRQ9U7C#vC;v)9`@jI~e(_0Ge0m)3E zUr7MJr9lO6`=i*^vrG`WZHekq|TM|B%uVXa(YFlQvP%^MSHGCd5e5tB@lUaFn4Ba2QtUCe&+6IlB3O(P+h zu1ZJM^$+{O!rUxoKl;I5D|MAb8J^5}O-rT?jq8;jxzU`P#mry+8J6z4{nsj)uAAPe zk~#N-g-!>vpZF-2?)+KAKN_CQd6-zE*Jav~vBsi`i~;Z3g_#e$w?~N9>}2Fw6DPh_n7&+n?V4s}vSRZCaxZD#tE zIoHBs#=@6BkNJQ26u8q}ahdblmP`w|hT2w3VCCoPJpLH=zV}^N`pGS&PI1-EUatIl z7&7NdtWF0D|MPR0`LOhat{9O@TIeYX3|PRzXT-B|qkR{`#>xMX@L zouTVuEiGaG)1SoLCqE9(Hh=h4$ef3XWq7dK#yc{bto*D9v+nEm9A#t78qB@oGE7`^ zt!mY@Qe4zZ^Kac<&tUdre}(pq-^%C+LVx$E>N@{wu(AnO{{%ijUG7D-N#h!VZBe&b zh1xjrWK6#49cY|zQdCG-v}BdZd5ReS*l|U#+0Vt^bB60Dw_x_;{~6r}?yI~LMOVWU z=JoIDN?_4b7oE#gX`}j|P{nheYiw$&kFBs>A(uNnjn?H?VEk3D?X@(ju`JeUXIYeD z8kY6qy?ZhLxld!^e|-_iE42H#`gLs6uNsSXzT{lx>PS>(L%zej?lnEqVGAcHMbeRy zL$SvdrNazH(K&MNuW`&)j9+;Tn$JJ4*PgQ^=T|1b?spQ^>ANb;GcPP)@n61;`G5En zx{o}hQo3?)v#7HWCP0MaO}}<5dg`KcIFL*mD-%ejOKlSiN`ZssPl06GQ+qzr%x08R zj}rPU=NhWsFSp?UTTK+_w7JY|`W6`MR&{#alEv+ve@@(HxRa|0H> z{uOi|eh}R0fFdhHbeW1_8#VGMVP$e1Qo3t3c~BscB~m%dn3au6rfo5Z`IMS?!A&OD zi(P+d3JVfAJ{drS3)yts(&&R=5b$2Vc| zyWhmpowxTIyhI)%DNRP~y1O-mqpbo*4J&i<*;o~kb7C_c$dg#oDxzeF`oSpaGF|Fw z01h$BG<9OznOF3VxxyBFOjcA)dn z{pj3&8@=ny1_9)@Jo!Jx@0(8i6R z@d=W z3)n4bU?}FS#@KDQXp3-@V1j~tLXnD2-I5y#N#qx!h``ZJEUu2HOR+j#@H5Z!Cd(s_ z6kau!3Imiskg+#{K!*`-f4D}lQWB{dU(>Xxrex7Z_DQ2TMv6t413CgN$Q~21TKSiuLA{XqOBtvT9ILZB%$oLAw zz+y(2!7z$Xl|f*?jUh3@J`ighE0s^SV+d@}CoNJ;G7V{k>=J0`0Hx_Si>BHVSGsWQ zQa>^$%8URjHiqG2iF8q(GAWf4i>0`r-~C#}N{Ps5Az1s{j&bvrO$z|YN1BBh19DC6 z+@5U_ZHJOf%W5WbEUPMHjipo|D}jZGoR}hw+1SoY7D0E3G<3UP^ z;yFjsUE_v>WtZ>~>E=ADSfklxTH(RaBk`mxml2{v#nRtkRJu8jD%Qw$nbw?Bxor1W zk0tE>h;(xvRjkyu$5iSUHtjM+4qKbkP%^EEe3cfY+LCFCg|ZZj2oXxAL*dErut>U! zgGrOuI@aiRnKqJX4%1@c1+6!R`qR}wFeP|E@Yrr5GL z94wAFAw<->PT4hKo4PlGfjkAbFJO&)m+7LehbnRIsQ9AJ(8I(U&2G-4ik12<(+~`I z@3nj)=;ecT3m!tmFO!mKTUnF$-j|Z;q5=v-6Id)kJ$MN>p@gs+0#>Zwm%mWGndZf`juawqTF|+%Y zY>p1b0E^eC=b`O8395UnhiDL=>R6u1D^7ym>ERV z*V!3`^uBl{y+1t`&5mm)0S7mO^Y@wEx%5Gz_w)CoPqI9|`W%}ZUOErNk@RuovD5El zY$o}BevR5QGK)*MpXKmek(pc=MDt{GlfIHxQ}J+_^oz0Ne zXy$NGkvKoMgI!!O^K1ffJ};{qEiZpM$;e$S`-wq$Sn}-ucy?^c&m>)5y9>3SX)h6scsGB9W0H#_6@V!q+KN~ z?3C?tt0#FkhQ;5UKgq`BX}8PtS@Ld?KBK;1?X69BA_Bm~p{?!iMuDB}ge12`$2%Lf zyJ^}s|KkPw5zG!C4_ICR@{6VWpB8W${oUZ6k_0Tv|+M# zQ%dir_`nWGqUkPW+#C+hUvoC2^T(4da;?Po#S@nklFWC~b1*hf2P2DnW+p6)O@B^p zZdke%Z2eprQ&DALXWO;UJdFaTtE~5rsNfRFZHORO@ocB`VpxQ5d18qpwlh6)v)k{G zzXi;#F|!F1yT$3)>>g0NOHuw@@(S4;Lj+c>&0_bFE|GhXc12TOKI@6i+GafvDQ)fZ zYV%NSy=F_D4n)%1KJoyi((Frsh<{%W&U6e$Mp(#fMWhG0+S(&GhuZa;w3A`%i>?<0 z<$kGnLB1eS6Ux^q&*MN9@-B$}mDN^B=5zA@{k;zltFKKVJshrf*+v$w-F(^Hz)Ujf zKIkn|#QY4mO|YDa2{B<25M-Bxh?Pshq-P_uaQ4}5P3*Qf7J&v;odE8@p;8E5aaAl- zS#fF1ITc>OcDta?@29=Upf>XPuyYq#2QAP6m))_rhH=b+&BN^LbUhd{Zi9DCTtx5A z=%E@EwsJKL%zc!zJw?K7gIW^*wlggMoAGi==ZF@DwEd-(fHeNDU`P#PmR{3AmcB+8 z6-%g)AhuU~%^m>FD;|sB%q$Y*$EZBkyxBa=TYiN3N*6=_E+>z2O?o6k*rFHewKx%z zu7fm29C|GGo_&Efhd?9oE2{ zIhT~Mv6)1h*yo%WJ{dMoHa9lUrQ8kTyt2uSfy}0&Tpe~>F)J)L&KO3~GTX(Qa+t?Vt1DS|)gljUebY_lTJV+no22PqL4ByE!Bk(qmJ?pwAQ?*2tLj$|^6wfhG$ zW|4d@?BGwean7O5Ll(1<

bzsdl^NJf%nk4K^L=g2Q&jC7nTvw`DRbf1YZ2Ra9$z z4hYBxUu3XLQY!|lfPyGmO?33GLR||rniol;E0lAuq(P|!bJ=XB#apFsukQ)#UC2Zb z5kTJGfx zQ>oDWSs6R#pp3+O?0rs!i0G7ZYm38`L^}eGLh=0GW(r*^ zeUI(6Y1D-jQlRfU>jYQHE%SJSpck;amU*&ynAbNE2oRasHT473Dr#0%mg_%T>{zF{ z^C0%`Lm(DpY*Cp-(rgV1R|rKZr8jtSYe#8EQfpU?xqCaJng1gqTiV%dFP9!Fu=K{Uq9 zRm9oPeirG~cx8>SMdtXujJp?L&0zJ5@O2lR1u1n(iwo;jP_3jAP9+0}vf9D&iaVb+ z4>Lw;Gc3Ut+6F&96BB&4Ut&Y1#S>bTMEZZz8`VcN0kx$FBM7DzGG>wYlAOm z8QJ@1f;!6gRpYkCWWQc|#kqJTmG0+-w_ASXI&8CBbG;BtaIi)G75TB(2NH1?RiQn0+#-x0A zKj@6R!vxNhKgB9z<72bQQF`n;PYC(1c0ov$Fmb!3S@uqfEht}mN)?kslPE|vu|b6h zB<{Z^aY=w|UTq#mnH>cM%6ySswMC_`;x?1X1hXXk$gQ6hX~?Hu4aQ5t_cqx@1ou=1 zxvCx#!7ik#S6F4!txXmQksNP}Vao`|;n~qPs<3#Iqf;mVq+e(FluUGeIn2Xk!*Xj^*2KjzMGw-IBo&0&`#zsG z57YJ?5pl9A$kd@HzkO&tP(pdEQ1B>iZ4F}f)Kc{xYi@zn-ypJECMTOY0Qa;n%mGkO z2FHU-1hL2%=IRO#;sTNF0&Bht+lQP7g4C|CyAz9So%Q7raBvcpm2|a9`?p8h=FjF~ zJK!w-n0z0VaiH|CipN+C8k73r3Qo)L$BI(jGQ-3H!H8V)LoeG=K2O^e7jLTX+Eh8| zxID@fqQ>2&I4PtmU7T&C76}{t`iU4-(6pI7jSU6GH*;xo14ACNIJ6=vk^Yy3k1TWX z%o?}WnaDy8N@bREm5F<;#1086MY{AfVn5B)306Qq)@d&j^mZ4~rh99HSpd{Dz;NiV zl{{&RZALDrAoa{~GC{vqMB2Q~944#oYMX~l#z{Abg-_+ky_Ed6WS*~H*)-9o^);PJ zhjg1b6^EtS$_!67F(e_!PbP~r>eM$y(u`)?OgshnYFZ}tTm?~M$%?cCN3J3wV6=e2 zJx|2&zW2xIA?IQD8{dHOH?L7JWt0@MtCQ`KySqB@A`BY{}m z)iw{ce@Th@NNswX0zXx99+W7|tlGtJBBWAAT)iOD>=MgliPHxdB>9BAa(k%2^Qix6 z@Nb=+%mcq7xD(;mw#-dT4BE043+G;dg^Mo5@QgEol`Yu#^v5x|;d({t(XNw5Z5&0) z5WEB<*;TftxWo*;vdxx|6yU@M$>yPq9SMHriXRg>YQ4?#2T9DJFf}Oc@eZ*$iWjjh zl?fGBUPG*rpHVp_wnTKBWbg$BY$ims1*dnn9g4*tc_J1rx)jZ^#{*3>U8%-n?0wfi z0+X?O=<1rqil`S2twmSNkVfg~KvvD)TPm-(!9cw$Z5~F7#w;QqBLi)ICtd9=2fK@G zeJOmDRFj#uNb10<5m=fmg)43B0~2b(W;UpdvxY9HtdIj!x_0YUEIjTKEIs9Eprej) zJILhbo3L@k=hdXl@Fx|6TAiBr!ihSON5#rM1O=-Ax_2S7kQ@w~hiU~ivXPQemj_l> z614-)?Lmy03nGpqXNclVYZymDaLguWPPyVirNt{hrE(tw6nX1P#NKXO*A(bRDdePJpSy=4umRAQ~Ht$1n zFd;0%*U%y>VvJl}m@GUPtzKFV$ZE3@5$LF6u>9=jW8s2FgBBMZ$tK;~^);-2^h4p~ zU|qwJ792guw1faJ+ax1B}jnIF|q0i_jc@g2&jc zyK(ES7~k;C&Rr(TY=+(~+3VEBr{ut94la@vaf_&nC4TU19y%E!qh15idLpr>toR61 z%DGUq^*Se9Y~yTFt8BI!W;4&~nhp|y;A%+0F{7w30laJ&muFZT8gAK&rKdd;i%)zi zXmP1yB2KS${LO2?cYe=I8tL{?Z=3zhZ|I| z|XV*aRW0dUvq7k(jO09&}XASh+|y91@xitP3l(5TE`` zN``V4?~^zkixrEg42hLe-{l`eezGNB)(!JHc0oLv8o2P7i?HR_UyZ@`6FQZETygT+ z8u-@lR_iDonTx#=6dRrZS2QxbEnK80g!kFq>w|0aQ1TTr3(CMZE{KUmSD=w-F-ytw zE83+xHT;q6IZkC%B;1D<`B24}Fxn8zh~4ZMo0He)R?ZpBR90CMD}2*n@v*DHv3r0}MChVWG;2h-+Z z;t4bSfK`4ds{yYmfSS~l1{EW0r0D>0Q85m21Jcd0<(W207EHjfaJM)SNdzz=undH3 z$g=O`RyY`7@yS1d+mAhPU%RNFp;PhfmSRSfMQ3XD9=M)WuC&0M_9}bVm7(V1&EdTUNfrZ7+@ASDgK?JV^ zz8pTMBB_jZ_b3^&aWw!7uDBYs-92n>M9u9L(h;=6kl!WqzHIp{L`7z3$JD~+u*0zOb1w%Sb#yp*BXUw{l~$<7!U8ZF z^;j{@>es@h1Ql58^90X}MA3xPGc32V@0Qa&cs4inK?bu@+L9OFYK=q?SHy;+mc_E{ zU!6Pd379gR?pUO4G&^~&rXZt3EL<+3bo$ec7wDHEr(t;o zw6tV)ihctHD!E!lRBS1NEau4av=lSyP^t~(yN%6_-7c&Lrxn|^A`S%+8+T^y$VeW* zVnSg+V7F>=(6W|XP?(AYT9`uOk-QA)YT3uP@aPM%aM7hjqK(Cy`})>Xp7b`*%9gZ4 zIT>zfG4OCZkK$Nr&CGA^@{RlYm8v*&FhFzM z_OJpvEhBc~MgFXO9^e9+L$3Kw#4Fb{1{oJnDBpFkZEi#)RMeC3!;jSRZ@}F&5G)rq z*TLHM4yfb6$-U8~S|r6FrAwoB8QtWj)ag~aGP=3Z^!3qs7hrVWBg%uZ;98a54__m~ z@IGhhay6r89JyXGW?sT>ZS7}vk*MG{szpU@_2ypO4Qy^w7O0T;U<4gqD{HLGr)e>B z)tN*b)6Z-!Gyt-QcnbPN^CI~l*tWq?6ouRp%;<)s__ST154#7JpYdE^I11g0ntNf< zjxNH|;N(+*nM-54^~$;gt0TzfezD1#GE7?$md`?ehu}$S`99}2qRzkv+vdjhuq8Q& zr8v3qjZ1ATT$02nR+8AqWZMeqCzIt=h<2nfyBMS6flOWT7dGX@Q z(42HiSSMJjAGGL)b>S=u(*8R*=~U1WN9GvT=qPJAOM&F32r}|dSuhv#GmtBA5`SS6 zBukG~fcB9T;lZ@IA)4jFO;xrOXAE-6>fub?3`)MMB(X&328i5tuiuZi?`T`2C_k^Z zN}ZqdE3CEME4cqVF3G+K9)|m0(G)Z#x9Td)>!Rf}p96yapMOQP2ah zZNQVz2!`XOw4$A*yn{w3sF-a%m^L?Z`*46c7aG2>M?pxekdf>cN4!^#w_@f>s6VM!SBo-chQE_rvPP0zc>yl{KN&9i&>2>B2MQmMpXyd=4o3uqGE&i#JEIICn$;o zS#B?sH9kV|7D)pX$4L&qT@B^A+Rv&I7E;J~TAfL&mQK%|nPYM#j;?+2!b{K`eRSyh z)RILd#KXanC3oe}!!SDcd@u=GrqJk?Hha0+*^(g7DgI^IhEzh6Fwf6W9ht;4OZAy5 z{F;64;M&|!P_6BtHiM;5g}Xt98WNIl$R}}NbQYY*pTRjvq@|e6l$CXmGW~4yr1fG$ zN>!6qwqW7HOP!j0R00&Oy!Lfe?uY#>5n=JtCxJ!_X=l-Dova2@LLyY_t$+z>AfV)v z8hD5i^_{U~Ep~0$vI76Xv$1&)+ z?=#C$61&I|wDQU+d{AG9VG2f^k>=hyr)8WMnt9aW=g#PEtc zT>c!Q&aF|WRt*km(%+PHBy&j=v9DAYri^Y3&v__lWlM+`N40>PsaB~4k`9v|46*c+ z&p>nRak<$}Bom;DPdQ+y*E&9-_8rr9~{dUqGu++@hCyXUjz zK2sr3lN@JulY{~m9>vfH6x903y;1Ki)tJ4Hq18iTjE`1^R#J@ z>i(+v&%WogEQzt8c%0)m+6lEDh2pi?*{85sBXjnZnV=Y9EOsk4l4C4H9AR2c z4}a+F#CF@CL`sBcL@RtPpq9TkTeo55#lL{TDW{o49*ciTmB#tvi1t4+tU^w&wf<|K zWMTrFLI}-b@GhU~h;|p*+~|JWFu%us3YiM<2$G7T+8WeH9b|0#eX@}@i`R@^2LTwI zbPAe751XPN;yOqYWtNsOy!U;osp*=}+=Xn^cq%27MDiG#*L`)+)FpwTXR7r^RpGDYPJGMA`n`3 zm>lhxg)5;2JPm?I^aHWEAp@2ZjMi^Zd95lA07d=F)dS|c63P@NOe?{*gNhYBPD(~Vs~CxRD($qaD_4AZo9_H7wKdbO*p4I4 z)kFu<=EluH2N_Q;xWj1Xe;3OO3-gc(?M4>CJ#b97K8?@~Zto+Q+PvrmrU~LT>S6(4B zpLL6gL`NJ9FgW)3qPtPVh%4wt?Xyw97p?X+NwM_Vror$*55d+~zZRo&AC~@g3IG5g z07*naQ~??cO&L{22`rkRCl3ii5t;I)T+$|MKc(z4$#N=n4vmZ?1|EF5-u~L$!1S{w z)x+6PIjN3FL_S|cA1wp&D55L2VmQpIHVu|9e<6nVdqDc^9 z2->=>Sb5Tgc=Y9Tl#ahY6C?#hM<0VNFaJ#}Kj-YO@ZiDx1SPG6(OlEda=E(zM6{ z7l|=Sc1mlW&t~zgzsX}S#=_$suZwDLzjfA?RTSBhcadmi1sIJwVYU4wBb5>|ef};j z7%JdgTE^m2pN_4+_9_fcI>qV469^^h29!9WA(&IpYNQSRjM9JVWh^Sj$=Jba)6beM zG!w?$SDPDx7L2Kv_*-I+PEuqjhbjpZN@_P`f)Dq*EFO>*!$JU}8bD-r=b890p;)gae!CYLdT{|fd^5Fuw+Z?iTxxrCoX7&=RSp}@w2 zgYvq>Y0?ADXrdvv1aO{;sEvO)8e#eJ7lH0^xLXCsH{M`~f+SL^V$dxwg9d}nRkTZm zzvfOvvIix%#lm(`8|CPCC0cXzG1&4ezlEirdKNGmLR0r6p-Gr>G*dcF^82F2f(r6U zJwu+gCQ0l#D^bCQxFjGtR%mxWn;S|)gre2l5#Uf9_WH9#3@UPLwPaWAbEWAe2gd09 zM`3i{1>peVJ8suKQALY5Rpd4t0o3$LjMRunwcxRaVAK?q`Y0|%lZ=@`i;Gx#=H*y< z$uG@FR|s0M<&mL<6@!)0x}Z|>!X(Rd^Uf&yEQEl-#pK;)Y;L5NUA<|7l_s0lbEK^y zFyctnQyRoV9mhv~j0k=vu|hY89)_i-Ju}N^+2gOSx_P2HFeOa2o}Mmc?P>$EOMSF- zoT&Xxsh8$l{DlCB>I5%byS#Y+8_zfd6$DC4z|c{E;%@0?97aqsv#X?JT25{4iG9L5*@*XJW}woEJcZGzi!>s^DV@#aW0lxidXj znH@Gaj^<%AcHv53YseKj6LGl$3ZIS0BZc#rT6#L-C@fz3#A)g`N@h$Ln2dv9&|eR4 ztOGph6%2OKNNa9DX|;`T=svJt7rDu%+(+kKAOx3X=j4()O#{m^FA+g?23|E zz2)Bo&5MQ+9Fm+2+ojFg+#uYLH~+)X{hpc8z0#q80CX#^V|4x_(Hwhx5mg|Xrn9lU z64uIDJ9+o+sn5+Z9oDhGI)c0sS~9ugcC7xx-(&5gAFQ6M4;H?e??=yJ;lfL>_~f4y zOg9zN$IW%a3KZEQPc5?HJ-421V(t}$vJwx3#i ztEo>67=Nt zdActODbbl+pV*$CT$kag%?+DOUIdGDRW7?ghrAdwk-w_6okl&usgP^|FgWAh7@T%S zG4huWZ*ctfu-}U#|BT0&+;a1#VYpgKJNoX%l~;hrV@$5U4r~AZZ#vP`lccoS^n3MM zmY1=7`3pf?wixsxIEO5^QoRDTe6c3oQwZCVJdaqu<9?|DnJhp=W>R3#4mn~iXVqU2 z0d}o)Gqhx_?gz*YvnGcGe(~SpEAqVe*}C7m<)sg-oA8Udn3P$wNdKo^>`BpY#){_#t7? zA~5vgn_B&dOg2~RS?J!9@bI#a7eviqmuo6*ZZNBujzs4OwsVzLA(A>}=7PbgATjqElx98LKNK(8!abL?@T(Snd*MN%)t5@ zCf~a~ZLxgE?O6TmH-c@$`bxI3R&ZO(a@OEJY8os)>BliR{Y*GzDzP{@$V_t4l`gdC z7ArG`E%QzSwk6>mr(Ia%xMZ7~G|t0z0bb`fCc_jmI1a$H{AxBXMX=%+L}hULz0e$U zTqjE{&G>s9j^>zS9fWB0a|porTh{~Y>z#gSR3a7mysjhGp55RbJCu<7+K1ndwf}fu zI08;R7Cw zg$o{|$Ur)L(K{zpB2hzfS_q4L*3tEpAls5fbdoja5BMG}zTIRego8v(L|E~hh~O@% z5*!qXO$aa`-GflMDuV$=4|+(ifh<>x(ZkLUc_LQmeX`^GnB4rGDz@7-k!upUJedIF zv40plcVO4={|?4CUf;PGR6Eh`LxF)h{&0w;r#%zRVfUz~-PP++UpM`_stIH-exG|- z`thZN8QE;)Y+8SUwFf;s>uk^=hxm*rWz7&G0IRDQU-gx$Pg+V_*9u#uuZqC{`i@M0pIm<( zcK!aVG5*d?y>>bqi1z(X7iQjUzb6(i{t;cBq%==hy>2sl(}EL%(-NNz%qe-_d`A$V z+#+B^HB+Zz%CTP_M>E0m+@)2j@lwJXl}v$^E?`}GAPuleI6C_z+e#qUn3*aW>30o| zJ04_KkCG3ow&0_W!63CtBS(Ibu+hesz5q-n#UQZcK9y`z?+-__HWOA=X!@D)l~-W* zYhIaliJDu}#fL6wd(axs-kR!XfO0ql!{GfcXM zQ2Nrd)VJ@~O(rK_ZC-C29x_!3VFpiS7mW*2lHUR_IQ5Lqgh#Zs5)l?JyaZ?loz3ev zuJ}B7_pWfO?-Dfc#D15S1m0^ZKW0B0S6+c#uY4KC*IiR=>e)5WOR!^~IH#5HU3~my zMKVx{)IfpX%__hwbz0M>vK_A2m1k(=OUfEF(JYW3hyo%Ob%BYbE#bNVt)N26+c{uL zoQ4z^V0h+zs@bwy5H)(pd1$tuP*9xD=bYSeJ2tNT;-*voHC{&gSzMYH;yMC=zPICR zuEx&a_$6$7=?hi#qi=w^*@O%cVd26{ft8iQ@bxykM&N4c;qjz&BoJz(v6`?VgUr&wheUn z5yi5kJ_MbG$6P#>`-1CVqf)c$AO8p@J9mbIH)6w;+^gtkc>0;5pL$jq()W0F-*B@|W#HsY zA2mIMbsxXDd3_XR>&EuQ(XSfJld5uHU5W$Uyq>mhLo+La^_X>cCXa|fsa$KQAV+&` zlUr{|T6m?^IfMuc2!i|>oO0Tfqs@WvU<8fr*@NAG`UhBj^Is`8U@awI<4{!Q5PrauSP*Qzc>m(6|gYi34s^syM+^Q0h7KtFfSxt;;4A=R0@ z=c9TQMhQTz*3<@t#Hwp(A6;nYn8(C8D3v6NwW9S6inXMhBaZ6SeY2#T5TaUM#`1H2 zdg{JNKaX?(G5OXFVfoo+*>!fpHAfwTfzqIkJ0`e+x4w?OZ+i*Z3enxfAF(&WMjJC>HihtiiPY**EV$37kl7ypO@@<9YF zEn)EqPpR@J_NTggaPPB3tz+&FlQYC*P+##3TJFcyR~4xRH9NZ|p8fpbl+!@VE2+iw zKH1#WyxxczsZM96dA-h3YNvY|6q0Y<%@KY~ntNIT$1U*fib+LQ$*tb5mP{mI7MH?g zM<2>$wEEC!gq0V(1dC7p3D6;jrpehHb1YW=`_E%IYfmV>C&FA^-Q!jH}^O^$sPpgeQ%pxEnqt|IWMRJaU_mUTUXcU!`T#_E;=Ji$e`T#$Tp`FOz z1|YWA(w1LJ7WCp-1S@i>A|=|&`#@#swZQ;`6HktP@KSWd{+^7lyA~T)d_LUWx`M@8 zQ0si^!;c6QuJ+All$2?ymKrxY|oq(nF~gh;|L^5&{rO8+2w&O_HZM{7C<}C}K^>Ht3xFPE1vy)<5x4 zEy&UnGw&)|kfbo$veiLw`)o7d+nJD{7wgJ6oyCRh*CNMO{(Pg`OBhsxtsg`>!Xy&~ zP$n1U#xO{XHev&m)6`3%P~%Qt^yItW0dH(ng}?itAhlqvB%>Q)xuxpyK2;=BRnUb3 zMyqQtwI;I&j%LT%_}Z6&J$s6UyCvkKkM4E)Z)q7A3{+gSKQ>1Vm}M=uYy($Pw#(y1 zLm7?T-pOS2V2B`K_)e@)omWJU4obK(5mX&o#=c$Yt=L?$+&k~w0lxEly$5Gkq9gKo zLtnLL@}%!i?hRu_<_+w{5^2)jC#00XE#Fnu>ZJrwUz@|%N23HY9tfLLfwXwOWbD?m z2%dx_A?VzPMK!OF5){69z2$CnH?K!f0IM6H%RErjyq;)Q9(CKT-L1|x0jJfzUxEeg z>s`kWD_EE@*|{UWAI{a3@3Gstc-Kx$ZoaAWe(cYG;7Jg*@4L-9fGh|(!nDVb-P&1; zG@9o1q_Z|Q#mTJ8V2pF0i%d0Q!>D+O>} z(q4VOUqU-d?gsM&lW*S$t*Sy{2VcuGQS7#Me%x_;F)LVN+I6Mq)!%FDxr*mN*_^&3 zx&_SoO)AxlR^gx9@tAiw)t`BG?k0^blg&wIlqKoyz$NWsD_ogHX*uO{>Mn4UvOsRi zwoPbE=U8UO`1VvU1Qk|Zc?;aou#diD3-*uchDzbreF`Ro)$t|{yAynUW~ z_ZGz=EzZ1|9_8t~dv*hpvF>5HpEf6~nnWI(boOLAW;t=}RehuEgdsW}OESasO_qt4 zY?5kiZ2EHBY>vhdWi_|)gE;DvmA!GJZ-epIzG7&G@!%{C`YyPtOMAKGTW?k`V{+5C zF}dNJ1%S#R1|~o!1g$(qqiUcVVdtBXIp7_4q7JHU-exi@x4Is~#4}NqngdA%t93b2 z^Lh#GDG~~a_P89Txl4ngZCflSfofh4!FL8JxXxnNU|@XX^(nzp>kPG-9x0Kl(nWx( z5taI+zSlqd>Fh(Z&1{j2df2veHjZDD(&!bxy(6Y5Qeh_(Om4YF58MM_Gn+0~3@`gn z(jFG@9}B42jMoMhwzFH6Y!>eDTMAbKSds~|=@1%J@~UITTF#T3zK!usH&z?QcTKPw zO44^luQji)@jTifZtcDA_ChUMolv7cR(Dxm#$W(C^spi=Aabi}8CN$k$K%;O7pvud z+N}6M#^+WtyiOg7K*G&X>P)QTIV6#vzBIIqH~Rl3!iVEQ&dJ zeQheMKsazVtBP$;cBHmag`(u6Q5?aT2)@r9sfQ_;hmXEm(c~TfjU?kv&YT;2OlU zApfW)@Uv#^M;|lO?yK)SU1&^cL7IH$rkSh*A26Hq<7SslWZBe(y--c=LY$$%JPt`T zuUA2wbQz*j88Vf?L46y7s2k`7^m`~NA7gy=*TCPqqj$zDdVn=Qxpd4v1*uh^b$uOs z{_;WU9OslVtY!h69}nGWCV&U{d9uOSrG+!`8O8kki6}_Fenyj_+gq^{-X^zb+VU zGsOJOs3S#gMjQ0*eb+m&_JQ{ZnOG6Ub2+`sCV|1SuynM5(K!zdd-!#c#k$<8_V10W zuSz@00kK(UF@nT-yOSCjT@G?L#ywS4-!3Twsas*?XGwLkRRfht1q(Xk)B(4Z#2;nH za|YHv_MxIyMHfkEGpl-68odw7!IKHrKl(xJ`SU-X3N=RMm}JwqtmFc0=XRbsjf0a; z#_(SEDJE4)iI&=ZQ!d+hjE&F#=K-}@GVyeul)&|Lc|0~mCDE(B#i3m{2-5hXuCOYy zmPHk>X6UKePZb0K77fW4$HdCM^-q0F@jAL(1p6SPm8+}LV_*0qV8+_}{uR4^|JA_W zRWV>J!Cle>peO;BHINXZrorN+PXd-#d>~hW?D`@>%u@l$$#-r#a5e``PF)2#>1L=7 zIg_ehDIm;+2cmg>0E~+YU#>#yvb0JNODfyA5)6@~4Fug|jrI=TejC<5_94XsuCd;t z*K3_wYY0i**B8g%wG(^(;!m*q4_*V_wcCmHs45Y;_iWP36NIlY??^P8B79K zkph)1HSGm-^ePaN8Dg^b{`Y{__PUif3V@=(r>52!p#-Hi6E5)D*ucgWpU2Ku{37MDjMT{Qw&|<2$Ru)zAHzTFduE9Pf@T_5c_1Ccaj<;g{ zlOF^2t|pg@MR}2|fzy*?#yo?NBxcDb%lCER(HCL#@JDrKJ-gzE+IPm+eq&l$E@P>E zv^n(zCsIP7senKPWNleiF6pyl&vw_6$0ZD5yBNunE0JFcD|eZLEFt^vXbv=|Xjln4 zS_LQ5H(EhGYirp1&bMQD-v>&wK(KSAAvL zVj2FL0k6*LRHYcYUro~C7Ky>r&cO2JFPvrw3oYPk=u%hPxONv@d*8c(+3vDGHs|}V z?@9Y1GRNW&b@Jl~qF6*u3g5imXz@}Q2(ixgaptr#STjfLzv8nBgCTaaj5J zUjg0Y@FMC`)BfvX-K9m~wp+3Gq4ysUo7-pYIM1dN8O!cu&gmBI1w(#yzRykKqo$S= zK)Hu#fj__^Vaz!2N-Nf8;AyjI_mfE~0$s(uWsd$9K(-s0M#)d)&8>Svdb zp&KAggM~|5XTIEcT6#dbyf18s}#gA|I8%p53jhQd^LsQf++hgi7yGHiSGYcV|i%ql0NRs~;cyR265+i%0(fBAp= zXS3xvL)4H`Zl&DvKuM^ITKLYkKnCab$PyNAyT8%x7oE@2Z?5mdW#p~}$CbZC==5E~ z_ZXdG@AACE(@w|M-+eurLl3Ja5^4{!G{{RqR`~krD)#>CKVkLle+RzhyY6~e2(l3Q z*A;oEX)rkT3@kn4IT$_oTwplt?El@Bo$b@!V)bwS46ASc>wU8sM#9CwbLjDXFbc{v z^|9C5Hu&XJPxwsYReHIoU`54gBGA6g&4BovP;E7Y~Oi+~#AQu;>Vv^6e94jyQ z84OOIi6sKPv^TmmHfjsZcAz3!0;j#^|BvVR+8NFg)YTsdT_5 z8`PHuY>hvju6Jz>yI%83tbgQ#{dY^2P%U-tq!i^~N{_rW4m!ph-FDG=oLjh&(yY^} z3sT2UYuZ75F4|=nx<3}&?<#IdKq?Q21|puI7mBOl+d-A zaUyti4<_IK7ACiRcPg?tnM|R#En6`-?)Yi{n1#ivf>oETn}Zznb*#Pj-Pry5*MK+H ztItuFPO;14lHlb~(5Av4TmO}Nz_aDkgjn94xyZ`V10cKEM zZDr~^SZdo|pURLf)&E-XR{MTm^`qLccy$kU|Ka}x-}+s}mB_%B>lzZDAELw64+#6581%N%eCbVp%mqpd|JDb z(-xC4Ea7g6va8~S^xkJvq?ljR5Y)*Lgcb} zee#D*X_c!9B$4e4#+18HQS*ADqRm68O4CY$$stA7uND4|bkvz-2oVukpvX)rjY*oQ zA{E)m?q9Z>Es=?^#qrnIu==LI0QRn80dNkF@#*Q$Q6#3CPu=B~bghWg!2@?nq?`1q zOR4$V1WOsU$pmZfdpCCf=^ujE*0Q*!s0WmRmI4Ie$v_Y}2N}e%szD4|nUDsh$@NSz zWF)s@#Wv4T^LooUsP!ck-&RaA~J#TyiCW5Tql?m(v@okpev@hQ|kj?9PJjU9; z{u6fp(d&TSJ3AY}$M^Zan%5tY=Jk=QX3OV2Ph@r@Cll;_$6K-cf4vHP%XfP#DsASc zSM&Og)x1k_e_xx|w^iqR-}Ywg{-f6ddv>EwBX|^t@7=d~J>7QE`Cfa}Chqr#Y|-&~ z)4pZ%B$7@3G}}+W%1d94(f#fpG?eV0ecLPy*vDt@n+@zcz}xpHJHC%SfAl)6ec(Oe z4%qA5Kvzq76zZBJ2!}3(Jb%=kc| z40}zs#zm3yV*A8kT>C(vb|Uopn;GM;UWwgrcf6o=~syN zNH>QM5QAzKj!i^c(UZKGci_9Vj7&I83{Xi^V{-S9sx3&@HCeq-W;dazqggzBD zeVBHt)^X=f?ERN_VDDT1cG~@b^)MpxD`HAHa}cQY@bR5JDod=hyRw3X6*Z6AX&)a^reLj?R616A-+E#z4brHaZBmg=qKPY;@ug7MwrC%3g8DuYntPsvr6>J379RCj(AI6eaNZhT9X+tV zt7{)u#trDn1mkaf9jkBs+f<|*VY;O)zvMP>B^;wZzJ4%Px}MMe*i5%ycpk^3AW~b0 zA}7fT;1+|FTdaJ|;EJ+AXHHvlS+~LBBOI{K;BC)Wn%yLlNgonmerwZUfWaAOV)3#k zV|4B#K-&&+K3;9L?Q$J98_d;?o34L-9ph`hj=lf<4y=9T1Hhg=<(;vUm5-1BB-N3k z@3_I5NM+`{g>%~Nc7JT9+b%qxvxYNOgSx68XJ!s_xne~HobbV>4dSXU4kU?18)9Y3 z>aL1DUvt8VSiI;`j2`(|G{+vdX*6TA1GXoQKN zfLxiPN#s{b`~2)Uu-Q9eo3-`#9>MV}r~QE09HAVwEvCY|?w6)j5i?M5tE-cx^jbe# z3iSi(c-iftt=lj->wZ{x)Z;LEz&U7+II`#tbRi+NV77AtFk|xl?_uLhU&PvnKY)!Z zzA$6R$=!>T{zspCfga?1pm#OYra{{SZ8P0=(fNYaBTB7Mcl=Jg@5 z+u`M`&n1=&h2S9EO0+P$D0Y~-MGffiBQQASRE!>S9tNl1Yues;c?B2@lmcj99&~Y1 zOeVm14BoX9)QD_c70(98nprxhhKyL=1h6GJ4GYfe zVRFmO7~gmU#@Akr$xYv$YRd^ZF@2vhJ51GkN)mksdN6ex^5-?#UP86{KFhJe)CU>4 zYi!N~dLJ`V`@Pp6pr2B8vR{y6_|SeoSo~1|Z>B$k0WcbYhNEef_VV(yPxithXgHj9 zW1Z>WVQnvX*G}-BJ;3@pczu292T#WFhRt!J!3RiPx7ngj`}tUE1|0;OhmIClXPkDir+cKJg zjq&t9+A}@+_wG z<-8>NE#(rmWDiRME32w@SAG<%Fn-s#`!#}}N1NxSd40KZLpvE+@ULA}DZ@RYo{eQ+ zatRBmx!Q_Pnra%B+obO0kwggDxJuGxbE2$w&qna`X!9^ZhqenMXP=m0lp#ya>tQo- zY--B1>%mGwL5eUE9C@2au{;!XXMNuQ4sG26VT5V8 z#N;ZHVl===pX)~Ok9&MhV=JhtyCVD9P5}=^&P##lOZp!bNnl{@| zz4fWRt1RzL{tR&`cRL@A;OEiip~9a{q^YWu5fN%g1$V`&X!SZNuf_n2ux-Dbugsja zsYR5l!U-0vy1?h*pt{*sKC}!oofR!rN85G8BxoGRL+f0-M)0ce#x#NtlM-b7S5Nu@ zN7e1{=s^$}&Xdja)x5sFxr)f|zr&>E!lv)Di?pqa1eotp-xR*ln?w7~~R`8rqx&PXga$qko* z92;I?b`PKm`sU5%Arw`ieMniLkxgpbcAACE1WEl3Gj&6Nc#m-UobR2rl2*G;_=!+bs&IMv)7N&5I)qXq8F!n)$HxvKQMN(*)eg_Qn>T`=N1KO4m6bAVULR)+ zv+ryQA$tnA$maFTdI`2I4+t7rw^>EIdbbfZnUWilJ_>ViEr2cREQ!-dzAqV9L&@_+ zwA{ADDQIl!rQL#q!srF6UV!CnYudnNGaq@c=g{UMyDAmwh}&x0KF#aZ3k6p|H?J@G z%cbV^vI<2NB}hNodJ1g}LFC|8LY!)p<6lFW2q(2A*XXJlliiY4IGbn$9|~H@JQqc2 zJQ2tWn9?_DidV*YvUx6>*UOum-4F?~mj%;h)p_oKsyapH=Ib*ALp1`ma3lM~lG4Uz zEpRzcjo|0e=0;+!3EPdbL(mVt*1TSMnaslHW}Vez0o`f1+9WzSr_D{?GvmhOkPo%0 zuxYt$WM+kAD z@bhT%TsN=RE{s@JNJ}y>M;WWKzBDC48-Y?#6z;P=zrmCabo2VVx)J<5+B^i4aw{av zzyWs4LDh}se8Bm_ntUa;^r*ftk>!2xC4*N-;bh%(R{L2bGPa!ikF!d@2%dJ1R#+Ca z$(lw9^pmj%m*R0 z_R@(hmrLNL9M5x6o!!D;Y;J_wn91ha;C$YBwRxEQRltk(TzjN``{W{-!g+C^UpJt0jD9 zO)wGdmp$2t$`P|ff#%HShFt>~%|(bh89VF{h+|XX->`g*D8LB}Ez`0lq$q{~)+NP} zW~opK=ct{_u#;qW1zl>UQariH#ibQ=PLX?*IS* literal 0 HcmV?d00001 diff --git a/client/public/icons/apple-touch-icon-57x57.png b/client/public/icons/apple-touch-icon-57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..b07d1da59d94de7a617ee11963889b7930ff4ced GIT binary patch literal 2579 zcmV+u3hecXP)6HM;ZTpbI;y;cenR$No@1jP2A?unxtu}!B-GN ziA`#4Q^e5Xiq(W#C{5IAL()H*ScD2iMI<0t6oi5gum!Oc0;V;TNCOqCo325k;e zMQ)TSWh*Nw^;=937-gRXEFdMpm(q zOd1(S#TLtAheJ!~8CK>pz<80!67mCv64P96fZ47L$smeInbJ_h+T!U{dwWsYv;{mh zh3SJY>l&iQYu4gd@=O4jSrb;GjjYF#!0VRG$(n5vqtdO8ITZ$*iWCH>Y~6nznF6JWHR<=mEu(|YL?K<>6{UcCcA7t1H zkB@@GuuvGt3ny&Ifi=X_X|S5Zuj`JF?t2#0 z-E9#h<64go0OHz;FhK9#{Sfs&yKk5`|7t7E^`B(r%MT*Bb(QmbX>d6}bj?A(=tz_W z+EcBL>O)`m4}S9kh^2$qM_t*3ZidU{7!0I=A-?C>opE;Vm3y~B)argg5TUZ=zL-W7 z$5?fbGIT2Zur{W0&jv(Pz6q$1(+aw!bBNW7qdQVji24^JkMia(6;tVZw?Zsf7=w*; zjCi0FfX5Tn92Hx}S2y>h*(7Na(czT>&$vji0!o9+Ao`ai8o|uY3)OD)eD5hB2%?o# zA%(&ZJAt+1wW49dzI=8RKvsMiy(-etY2hN&o;!fb1KXVZxN<_SwnEOG6*;iypOp~Oz5P*$S{?1i zgifPW@=hc-#&&g~`?2q$`tTzK^hxK<#;;)M#o`SQ$KI4!{ubar46H8w26VVR0JQ$~7I<>f4VdSDBx*Nu z42>rrM|#Rh7sVl zcuQtRY2|HEj#M$Q@boE6|M?|UwhcLfgBVy^_?#pgLaY}#CB@@o8UyNtNeEC^m-fvB z1)|mm2$7*b^ZH+PTBaat_0Cq(y=rXDH#30n-IJE^$4&;#=k|qhpxw^UZl65^Znr@~ zB&)2^X|+09$$*KnCjhJ;u%y}O=tX$^9q`Ny5U-duqu}|?96l5$%heMb26gvnYHZR= zL5A%>gq+EDA;FLuGGL+!Bb;!viPp$)N4T_o?tM)E@wW+GH8cgQ)_D7dCq@-BBD-lx zUYX4h0x+eaMBB}0oO1MS_Vw51KEe0Tp!xJ}v>S~~XaW(+TRbU~>1nh^h8;n}7f@Kq z46ws;T`G6zAb7G_c=e`!^E@UV-;VIq344}(<0b^_);np;9z6oSJlR>cjyPur>SDYx zf$5k2P%wqbn0@mNz``UEp}cVudY^d8+kfiC-(xOC9NHKc?*1%0peOZuO7tAUk)NXbsQ)K2-d7c<(~Ty+_~O6&g|hsn0$6$VaJx?Gpfk?5Q*C* zA4Tn_zi=J}eAr)md<+vicYr^*)IkQEuFf$*`}8R^_wA0e16O&1%?6s!?CoHJ#x5jH zQ!56^Y)N9;p5t@Ikz;7?-ih$N|K_TcD+Zcgy*)mL=HBlheCOYDOs!?#34dvkZ?d|y z!V{xt?An2uSN;SJ!^~t{lh3?o#_ZuknAo`^o*+50utpeQSH_lOo0L@+2~sc$WDF)! zbdeS$sNYJtqAVu)HFg7u$5uzD>@14|+1%?EmVA{H)Bg3q5v z`0h!xMuyS)$D4^=Yh+nHk1-INDsW4k3~AogfnBX2fh{=El%B61NMhkkzQw^M8+3ie zeHwO6@pfS4(>Qxztqe*z8zDFW6JRe^QVy();}ptP+prUgC;1DC6I+@*jJ|~ie(3RlXzXVVQ)28r563e=OkbmyZ?E3ZJ$gKwGKWt}eTaA~-2IE@Z@ z3kcQXK$eOCH?$?^##-&kR@qt#2y;*FIb#Cs*(__DX39YgZP)hvRbf*@M} z1SaF?#Nvru_`+Nj&e_oTj^onv4NHH9an?cFskB**l@_H*$CCtfhZSgnoHIt58oq7~ zq?HC6wyszs#Wcw%IVGnPBFz~|FrX&JlXwr3M|V2^?0{dn_G5Hrr7DB9Y7RY|sL)xS pxunZY#%WgCjt!`-`3t4?;eWk|mdmBFm`4Br002ovPDHLkV1h8T`P={i literal 0 HcmV?d00001 diff --git a/client/public/icons/apple-touch-icon-72x72.png b/client/public/icons/apple-touch-icon-72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..0ebf2c8ccfdf80922cf7ab5ff18512dd0a5e9a83 GIT binary patch literal 3311 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 literal 0 HcmV?d00001 diff --git a/client/public/icons/apple-touch-icon-76x76.png b/client/public/icons/apple-touch-icon-76x76.png new file mode 100644 index 0000000000000000000000000000000000000000..d636fe97d49405bb55f3a1062d40fdf5b0cf266c GIT binary patch literal 4058 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 literal 0 HcmV?d00001 diff --git a/client/public/favicon.ico b/client/public/icons/favicon.ico similarity index 100% rename from client/public/favicon.ico rename to client/public/icons/favicon.ico diff --git a/client/public/index.html b/client/public/index.html index c93d95e..32e17fe 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -2,7 +2,51 @@ - + + + + + + + + + + { + const { pinCategoriesByDefault: pinCategories } = await loadConfig(); + + let category; + + if (pinCategories) { + category = await Category.create({ + ...req.body, + isPinned: true, + }); + } else { + category = await Category.create(req.body); + } + + res.status(201).json({ + success: true, + data: category, + }); +}); + +module.exports = createCategory; diff --git a/controllers/categories/deleteCategory.js b/controllers/categories/deleteCategory.js new file mode 100644 index 0000000..e9b004b --- /dev/null +++ b/controllers/categories/deleteCategory.js @@ -0,0 +1,45 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const ErrorResponse = require('../../utils/ErrorResponse'); +const Category = require('../../models/Category'); +const Bookmark = require('../../models/Bookmark'); + +// @desc Delete category +// @route DELETE /api/categories/:id +// @access Public +const deleteCategory = asyncWrapper(async (req, res, next) => { + const category = await Category.findOne({ + where: { id: req.params.id }, + include: [ + { + model: Bookmark, + as: 'bookmarks', + }, + ], + }); + + if (!category) { + return next( + new ErrorResponse( + `Category with id of ${req.params.id} was not found`, + 404 + ) + ); + } + + category.bookmarks.forEach(async (bookmark) => { + await Bookmark.destroy({ + where: { id: bookmark.id }, + }); + }); + + await Category.destroy({ + where: { id: req.params.id }, + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +module.exports = deleteCategory; diff --git a/controllers/categories/getAllCategories.js b/controllers/categories/getAllCategories.js new file mode 100644 index 0000000..597bfcc --- /dev/null +++ b/controllers/categories/getAllCategories.js @@ -0,0 +1,43 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const Category = require('../../models/Category'); +const Bookmark = require('../../models/Bookmark'); +const { Sequelize } = require('sequelize'); +const loadConfig = require('../../utils/loadConfig'); + +// @desc Get all categories +// @route GET /api/categories +// @access Public +const getAllCategories = asyncWrapper(async (req, res, next) => { + const { useOrdering: orderType } = await loadConfig(); + + let categories; + + if (orderType == 'name') { + categories = await Category.findAll({ + include: [ + { + model: Bookmark, + as: 'bookmarks', + }, + ], + order: [[Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC']], + }); + } else { + categories = await Category.findAll({ + include: [ + { + model: Bookmark, + as: 'bookmarks', + }, + ], + order: [[orderType, 'ASC']], + }); + } + + res.status(200).json({ + success: true, + data: categories, + }); +}); + +module.exports = getAllCategories; diff --git a/controllers/categories/getSingleCategory.js b/controllers/categories/getSingleCategory.js new file mode 100644 index 0000000..084362b --- /dev/null +++ b/controllers/categories/getSingleCategory.js @@ -0,0 +1,35 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const ErrorResponse = require('../../utils/ErrorResponse'); +const Category = require('../../models/Category'); +const Bookmark = require('../../models/Bookmark'); + +// @desc Get single category +// @route GET /api/categories/:id +// @access Public +const getSingleCategory = asyncWrapper(async (req, res, next) => { + const category = await Category.findOne({ + where: { id: req.params.id }, + include: [ + { + model: Bookmark, + as: 'bookmarks', + }, + ], + }); + + if (!category) { + return next( + new ErrorResponse( + `Category with id of ${req.params.id} was not found`, + 404 + ) + ); + } + + res.status(200).json({ + success: true, + data: category, + }); +}); + +module.exports = getSingleCategory; diff --git a/controllers/categories/index.js b/controllers/categories/index.js new file mode 100644 index 0000000..8b3c179 --- /dev/null +++ b/controllers/categories/index.js @@ -0,0 +1,8 @@ +module.exports = { + createCategory: require('./createCategory'), + getAllCategories: require('./getAllCategories'), + getSingleCategory: require('./getSingleCategory'), + updateCategory: require('./updateCategory'), + deleteCategory: require('./deleteCategory'), + reorderCategories: require('./reorderCategories'), +}; diff --git a/controllers/categories/reorderCategories.js b/controllers/categories/reorderCategories.js new file mode 100644 index 0000000..492675b --- /dev/null +++ b/controllers/categories/reorderCategories.js @@ -0,0 +1,22 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const Category = require('../../models/Category'); +// @desc Reorder categories +// @route PUT /api/categories/0/reorder +// @access Public +const reorderCategories = asyncWrapper(async (req, res, next) => { + req.body.categories.forEach(async ({ id, orderId }) => { + await Category.update( + { orderId }, + { + where: { id }, + } + ); + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +module.exports = reorderCategories; diff --git a/controllers/categories/updateCategory.js b/controllers/categories/updateCategory.js new file mode 100644 index 0000000..cc43db6 --- /dev/null +++ b/controllers/categories/updateCategory.js @@ -0,0 +1,30 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const ErrorResponse = require('../../utils/ErrorResponse'); +const Category = require('../../models/Category'); + +// @desc Update category +// @route PUT /api/categories/:id +// @access Public +const updateCategory = asyncWrapper(async (req, res, next) => { + let category = await Category.findOne({ + where: { id: req.params.id }, + }); + + if (!category) { + return next( + new ErrorResponse( + `Category with id of ${req.params.id} was not found`, + 404 + ) + ); + } + + category = await category.update({ ...req.body }); + + res.status(200).json({ + success: true, + data: category, + }); +}); + +module.exports = updateCategory; diff --git a/controllers/category.js b/controllers/category.js deleted file mode 100644 index d10183f..0000000 --- a/controllers/category.js +++ /dev/null @@ -1,178 +0,0 @@ -const asyncWrapper = require('../middleware/asyncWrapper'); -const ErrorResponse = require('../utils/ErrorResponse'); -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) => { - const { pinCategoriesByDefault: pinCategories } = await loadConfig(); - - let category; - - if (pinCategories) { - category = await Category.create({ - ...req.body, - isPinned: true, - }); - } else { - category = await Category.create(req.body); - } - - res.status(201).json({ - success: true, - data: category, - }); -}); - -// @desc Get all categories -// @route GET /api/categories -// @access Public -exports.getCategories = asyncWrapper(async (req, res, next) => { - const { useOrdering: orderType } = await loadConfig(); - - let categories; - - if (orderType == 'name') { - categories = await Category.findAll({ - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - order: [[Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC']], - }); - } else { - categories = await Category.findAll({ - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - order: [[orderType, 'ASC']], - }); - } - - res.status(200).json({ - success: true, - data: categories, - }); -}); - -// @desc Get single category -// @route GET /api/categories/:id -// @access Public -exports.getCategory = asyncWrapper(async (req, res, next) => { - const category = await Category.findOne({ - where: { id: req.params.id }, - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - }); - - if (!category) { - return next( - new ErrorResponse( - `Category with id of ${req.params.id} was not found`, - 404 - ) - ); - } - - res.status(200).json({ - success: true, - data: category, - }); -}); - -// @desc Update category -// @route PUT /api/categories/:id -// @access Public -exports.updateCategory = asyncWrapper(async (req, res, next) => { - let category = await Category.findOne({ - where: { id: req.params.id }, - }); - - if (!category) { - return next( - new ErrorResponse( - `Category with id of ${req.params.id} was not found`, - 404 - ) - ); - } - - category = await category.update({ ...req.body }); - - res.status(200).json({ - success: true, - data: category, - }); -}); - -// @desc Delete category -// @route DELETE /api/categories/:id -// @access Public -exports.deleteCategory = asyncWrapper(async (req, res, next) => { - const category = await Category.findOne({ - where: { id: req.params.id }, - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - }); - - if (!category) { - return next( - new ErrorResponse( - `Category with id of ${req.params.id} was not found`, - 404 - ) - ); - } - - category.bookmarks.forEach(async (bookmark) => { - await Bookmark.destroy({ - where: { id: bookmark.id }, - }); - }); - - await Category.destroy({ - where: { id: req.params.id }, - }); - - res.status(200).json({ - success: true, - data: {}, - }); -}); - -// @desc Reorder categories -// @route PUT /api/categories/0/reorder -// @access Public -exports.reorderCategories = asyncWrapper(async (req, res, next) => { - req.body.categories.forEach(async ({ id, orderId }) => { - await Category.update( - { orderId }, - { - where: { id }, - } - ); - }); - - res.status(200).json({ - success: true, - data: {}, - }); -}); diff --git a/controllers/queries/addQuery.js b/controllers/queries/addQuery.js new file mode 100644 index 0000000..cd61c67 --- /dev/null +++ b/controllers/queries/addQuery.js @@ -0,0 +1,21 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); + +// @desc Add custom search query +// @route POST /api/queries +// @access Public +const addQuery = asyncWrapper(async (req, res, next) => { + const file = new File('data/customQueries.json'); + let content = JSON.parse(file.read()); + + // Add new query + content.queries.push(req.body); + file.write(content, true); + + res.status(201).json({ + success: true, + data: req.body, + }); +}); + +module.exports = addQuery; diff --git a/controllers/queries/deleteQuery.js b/controllers/queries/deleteQuery.js new file mode 100644 index 0000000..1a30041 --- /dev/null +++ b/controllers/queries/deleteQuery.js @@ -0,0 +1,22 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); + +// @desc Delete query +// @route DELETE /api/queries/:prefix +// @access Public +const deleteQuery = asyncWrapper(async (req, res, next) => { + const file = new File('data/customQueries.json'); + let content = JSON.parse(file.read()); + + content.queries = content.queries.filter( + (q) => q.prefix != req.params.prefix + ); + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +module.exports = deleteQuery; diff --git a/controllers/queries/getQueries.js b/controllers/queries/getQueries.js new file mode 100644 index 0000000..6299473 --- /dev/null +++ b/controllers/queries/getQueries.js @@ -0,0 +1,17 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); + +// @desc Get custom queries file +// @route GET /api/queries +// @access Public +const getQueries = asyncWrapper(async (req, res, next) => { + const file = new File('data/customQueries.json'); + const content = JSON.parse(file.read()); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +module.exports = getQueries; diff --git a/controllers/queries/index.js b/controllers/queries/index.js index ae1ccec..3d5d036 100644 --- a/controllers/queries/index.js +++ b/controllers/queries/index.js @@ -1,81 +1,6 @@ -const asyncWrapper = require('../../middleware/asyncWrapper'); -const File = require('../../utils/File'); -const { join } = require('path'); - -const QUERIES_PATH = join(__dirname, '../../data/customQueries.json'); - -// @desc Add custom search query -// @route POST /api/queries -// @access Public -exports.addQuery = asyncWrapper(async (req, res, next) => { - const file = new File(QUERIES_PATH); - let content = JSON.parse(file.read()); - - // Add new query - content.queries.push(req.body); - file.write(content, true); - - res.status(201).json({ - success: true, - data: req.body, - }); -}); - -// @desc Get custom queries file -// @route GET /api/queries -// @access Public -exports.getQueries = asyncWrapper(async (req, res, next) => { - const file = new File(QUERIES_PATH); - const content = JSON.parse(file.read()); - - res.status(200).json({ - success: true, - data: content.queries, - }); -}); - -// @desc Update query -// @route PUT /api/queries/:prefix -// @access Public -exports.updateQuery = asyncWrapper(async (req, res, next) => { - const file = new File(QUERIES_PATH); - let content = JSON.parse(file.read()); - - let queryIdx = content.queries.findIndex( - (q) => q.prefix == req.params.prefix - ); - - // query found - if (queryIdx > -1) { - content.queries = [ - ...content.queries.slice(0, queryIdx), - req.body, - ...content.queries.slice(queryIdx + 1), - ]; - } - - file.write(content, true); - - res.status(200).json({ - success: true, - data: content.queries, - }); -}); - -// @desc Delete query -// @route DELETE /api/queries/:prefix -// @access Public -exports.deleteQuery = asyncWrapper(async (req, res, next) => { - const file = new File(QUERIES_PATH); - let content = JSON.parse(file.read()); - - content.queries = content.queries.filter( - (q) => q.prefix != req.params.prefix - ); - file.write(content, true); - - res.status(200).json({ - success: true, - data: content.queries, - }); -}); +module.exports = { + addQuery: require('./addQuery'), + getQueries: require('./getQueries'), + updateQuery: require('./updateQuery'), + deleteQuery: require('./deleteQuery'), +}; diff --git a/controllers/queries/updateQuery.js b/controllers/queries/updateQuery.js new file mode 100644 index 0000000..a95b71a --- /dev/null +++ b/controllers/queries/updateQuery.js @@ -0,0 +1,32 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); + +// @desc Update query +// @route PUT /api/queries/:prefix +// @access Public +const updateQuery = asyncWrapper(async (req, res, next) => { + const file = new File('data/customQueries.json'); + let content = JSON.parse(file.read()); + + let queryIdx = content.queries.findIndex( + (q) => q.prefix == req.params.prefix + ); + + // query found + if (queryIdx > -1) { + content.queries = [ + ...content.queries.slice(0, queryIdx), + req.body, + ...content.queries.slice(queryIdx + 1), + ]; + } + + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +module.exports = updateQuery; diff --git a/controllers/weather.js b/controllers/weather.js deleted file mode 100644 index 3acd1ad..0000000 --- a/controllers/weather.js +++ /dev/null @@ -1,31 +0,0 @@ -const asyncWrapper = require('../middleware/asyncWrapper'); -const ErrorResponse = require('../utils/ErrorResponse'); -const Weather = require('../models/Weather'); -const getExternalWeather = require('../utils/getExternalWeather'); - -// @desc Get latest weather status -// @route GET /api/weather -// @access Public -exports.getWeather = asyncWrapper(async (req, res, next) => { - const weather = await Weather.findAll({ - order: [['createdAt', 'DESC']], - limit: 1, - }); - - res.status(200).json({ - success: true, - data: weather, - }); -}); - -// @desc Update weather -// @route GET /api/weather/update -// @access Public -exports.updateWeather = asyncWrapper(async (req, res, next) => { - const weather = await getExternalWeather(); - - res.status(200).json({ - success: true, - data: weather, - }); -}); diff --git a/controllers/weather/getWather.js b/controllers/weather/getWather.js new file mode 100644 index 0000000..44e6e3f --- /dev/null +++ b/controllers/weather/getWather.js @@ -0,0 +1,19 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const Weather = require('../../models/Weather'); + +// @desc Get latest weather status +// @route GET /api/weather +// @access Public +const getWeather = asyncWrapper(async (req, res, next) => { + const weather = await Weather.findAll({ + order: [['createdAt', 'DESC']], + limit: 1, + }); + + res.status(200).json({ + success: true, + data: weather, + }); +}); + +module.exports = getWeather; diff --git a/controllers/weather/index.js b/controllers/weather/index.js new file mode 100644 index 0000000..8c7231d --- /dev/null +++ b/controllers/weather/index.js @@ -0,0 +1,4 @@ +module.exports = { + getWeather: require('./getWather'), + updateWeather: require('./updateWeather'), +}; diff --git a/controllers/weather/updateWeather.js b/controllers/weather/updateWeather.js new file mode 100644 index 0000000..c66417e --- /dev/null +++ b/controllers/weather/updateWeather.js @@ -0,0 +1,16 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const getExternalWeather = require('../../utils/getExternalWeather'); + +// @desc Update weather +// @route GET /api/weather/update +// @access Public +const updateWeather = asyncWrapper(async (req, res, next) => { + const weather = await getExternalWeather(); + + res.status(200).json({ + success: true, + data: weather, + }); +}); + +module.exports = updateWeather; diff --git a/routes/category.js b/routes/category.js index 64067d7..b7527c8 100644 --- a/routes/category.js +++ b/routes/category.js @@ -3,26 +3,21 @@ const router = express.Router(); const { createCategory, - getCategories, - getCategory, + getAllCategories, + getSingleCategory, updateCategory, deleteCategory, - reorderCategories -} = require('../controllers/category'); + reorderCategories, +} = require('../controllers/categories'); -router - .route('/') - .post(createCategory) - .get(getCategories); +router.route('/').post(createCategory).get(getAllCategories); router .route('/:id') - .get(getCategory) + .get(getSingleCategory) .put(updateCategory) .delete(deleteCategory); -router - .route('/0/reorder') - .put(reorderCategories); +router.route('/0/reorder').put(reorderCategories); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/utils/clearWeatherData.js b/utils/clearWeatherData.js index 07be15b..5e4972a 100644 --- a/utils/clearWeatherData.js +++ b/utils/clearWeatherData.js @@ -2,23 +2,28 @@ const { Op } = require('sequelize'); const Weather = require('../models/Weather'); const Logger = require('./Logger'); const logger = new Logger(); +const loadConfig = require('./loadConfig'); const clearWeatherData = async () => { + const { WEATHER_API_KEY: secret } = await loadConfig(); + const weather = await Weather.findOne({ - order: [[ 'createdAt', 'DESC' ]] + order: [['createdAt', 'DESC']], }); if (weather) { await Weather.destroy({ where: { id: { - [Op.lt]: weather.id - } - } - }) + [Op.lt]: weather.id, + }, + }, + }); } - logger.log('Old weather data was deleted'); -} + if (secret) { + logger.log('Old weather data was deleted'); + } +}; -module.exports = clearWeatherData; \ No newline at end of file +module.exports = clearWeatherData; From 4e205278341967dc7823d2e15c33c92cddae8408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 5 Nov 2021 15:05:33 +0100 Subject: [PATCH 058/166] Added new themes --- client/src/components/Themer/themes.json | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/client/src/components/Themer/themes.json b/client/src/components/Themer/themes.json index 812191f..f3b12bd 100644 --- a/client/src/components/Themer/themes.json +++ b/client/src/components/Themer/themes.json @@ -95,6 +95,30 @@ "primary": "#4C432E", "accent": "#AA9A73" } + }, + { + "name": "neon", + "colors": { + "background": "#091833", + "primary": "#EFFBFF", + "accent": "#ea00d9" + } + }, + { + "name": "pumpkin", + "colors": { + "background": "#2d3436", + "primary": "#EFFBFF", + "accent": "#ffa500" + } + }, + { + "name": "onedark", + "colors": { + "background": "#282c34", + "primary": "#dfd9d6", + "accent": "#98c379" + } } ] -} \ No newline at end of file +} From aca8b0261e28d3be56185220415ce3fe0ca788dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 5 Nov 2021 16:39:42 +0100 Subject: [PATCH 059/166] Added option to set custom greetings. Moved HomeHeader to separate file. Cleaned up README file --- CHANGELOG.md | 2 + README.md | 57 +++++++------------ .../components/Home/Header/Header.module.css | 31 ++++++++++ client/src/components/Home/Header/Header.tsx | 49 ++++++++++++++++ .../functions/getDateTime.ts} | 2 +- .../Home/Header/functions/greeter.ts | 17 ++++++ client/src/components/Home/Home.module.css | 32 +---------- client/src/components/Home/Home.tsx | 43 +------------- .../src/components/Home/functions/greeter.ts | 12 ---- .../Settings/OtherSettings/OtherSettings.tsx | 15 +++++ client/src/interfaces/Config.ts | 1 + client/src/interfaces/Forms.ts | 1 + client/src/store/actions/config.ts | 2 + .../utility/templateObjects/configTemplate.ts | 1 + .../templateObjects/settingsTemplate.ts | 1 + utils/ErrorResponse.js | 4 +- utils/init/initialConfig.json | 3 +- 17 files changed, 149 insertions(+), 124 deletions(-) create mode 100644 client/src/components/Home/Header/Header.module.css create mode 100644 client/src/components/Home/Header/Header.tsx rename client/src/components/Home/{functions/dateTime.ts => Header/functions/getDateTime.ts} (94%) create mode 100644 client/src/components/Home/Header/functions/greeter.ts delete mode 100644 client/src/components/Home/functions/greeter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index afd7297..1011cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### v1.7.4 (TBA) +- [WIP] Added option to set custom greetings and date ([#103](https://github.com/pawelmalak/flame/issues/103)) - Added iOS "Add to homescreen" icon ([#131](https://github.com/pawelmalak/flame/issues/131)) +- Added 3 new themes ### v1.7.3 (2021-10-28) - Fixed bug with custom CSS not updating diff --git a/README.md b/README.md index e3fd2d7..0fcf509 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ # Flame -[![JS Badge](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black)](https://shields.io/) -[![TS Badge](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://shields.io/) -[![Node Badge](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://shields.io/) -[![React Badge](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://shields.io/) - ![Homescreen screenshot](./.github/_home.png) ## Description -Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own appliaction hub in no time - no file editing necessary. +Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own application hub in no time - no file editing necessary. ## Technology @@ -42,7 +37,15 @@ npm run dev ### With Docker (recommended) -[Docker Hub](https://hub.docker.com/r/pawelmalak/flame) +[Docker Hub link](https://hub.docker.com/r/pawelmalak/flame) + +```sh +docker pull pawelmalak/flame:latest + +# for ARM architecture (e.g. RaspberryPi) +docker pull pawelmalak/flame:multiarch +``` + #### Building images @@ -96,13 +99,13 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ - Applications - Create, update, delete and organize applications using GUI - - Pin your favourite apps to homescreen + - Pin your favourite apps to the homescreen ![Homescreen screenshot](./.github/_apps.png) - Bookmarks - Create, update, delete and organize bookmarks and categories using GUI - - Pin your favourite categories to homescreen + - Pin your favourite categories to the homescreen ![Homescreen screenshot](./.github/_bookmarks.png) @@ -111,7 +114,7 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ - Get current temperature, cloud coverage and weather status with animated icons - Themes - - Customize your page by choosing from 12 color themes + - Customize your page by choosing from 15 color themes ![Homescreen screenshot](./.github/_themes.png) @@ -125,23 +128,7 @@ To use search bar you need to type your search query with selected prefix. For e > You can change where to open search results (same/new tab) in the settings -#### Supported search engines - -| Name | Prefix | Search URL | -| ---------- | ------ | ----------------------------------- | -| Disroot | /ds | http://search.disroot.org/search?q= | -| DuckDuckGo | /d | https://duckduckgo.com/?q= | -| Google | /g | https://www.google.com/search?q= | - -#### Supported services - -| Name | Prefix | Search URL | -| ------------------ | ------ | --------------------------------------------- | -| IMDb | /im | https://www.imdb.com/find?q= | -| Reddit | /r | https://www.reddit.com/search?q= | -| Spotify | /sp | https://open.spotify.com/search/ | -| The Movie Database | /mv | https://www.themoviedb.org/search?query= | -| Youtube | /yt | https://www.youtube.com/results?search_query= | +For list of supported search engines, shortcuts and more about searching functionality visit [project wiki](https://github.com/pawelmalak/flame/wiki/Search-bar). ### Setting up weather module @@ -159,13 +146,13 @@ labels: - flame.type=application # "app" works too - flame.name=My container - flame.url=https://example.com - - flame.icon=icon-name # Optional, default is "docker" + - flame.icon=icon-name # optional, default is "docker" # - flame.icon=custom to make changes in app. ie: custom icon upload ``` -And you must have activated the Docker sync option in the settings panel. +> "Use Docker API" option must be enabled for this to work. You can find it in Settings > Other > Docker section -You can set up different apps in the same label adding `;` between each one. +You can also set up different apps in the same label adding `;` between each one. ```yml labels: @@ -208,13 +195,11 @@ metadata: - flame.pawelmalak/type=application # "app" works too - flame.pawelmalak/name=My container - flame.pawelmalak/url=https://example.com - - flame.pawelmalak/icon=icon-name # Optional, default is "kubernetes" + - flame.pawelmalak/icon=icon-name # optional, default is "kubernetes" ``` -And you must have activated the Kubernetes sync option in the settings panel. +> "Use Kubernetes Ingress API" option must be enabled for this to work. You can find it in Settings > Other > Kubernetes section -### Custom CSS +### Custom CSS and themes -> This is an experimental feature. Its behaviour might change in the future. -> -> Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) \ No newline at end of file +See project wiki for [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) and [Custom theme with CSS](https://github.com/pawelmalak/flame/wiki/Custom-theme-with-CSS). \ No newline at end of file diff --git a/client/src/components/Home/Header/Header.module.css b/client/src/components/Home/Header/Header.module.css new file mode 100644 index 0000000..d7ee22b --- /dev/null +++ b/client/src/components/Home/Header/Header.module.css @@ -0,0 +1,31 @@ +.Header h1 { + color: var(--color-primary); + font-weight: 700; + font-size: 4em; + display: inline-block; +} + +.Header p { + color: var(--color-primary); + font-weight: 300; + text-transform: uppercase; + height: 30px; +} + +.HeaderMain { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2.5rem; +} + +.SettingsLink { + visibility: visible; + color: var(--color-accent); +} + +@media (min-width: 769px) { + .SettingsLink { + visibility: hidden; + } +} diff --git a/client/src/components/Home/Header/Header.tsx b/client/src/components/Home/Header/Header.tsx new file mode 100644 index 0000000..3b2841b --- /dev/null +++ b/client/src/components/Home/Header/Header.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { Config, GlobalState } from '../../../interfaces'; +import WeatherWidget from '../../Widgets/WeatherWidget/WeatherWidget'; +import { getDateTime } from './functions/getDateTime'; +import { greeter } from './functions/greeter'; +import classes from './Header.module.css'; + +interface Props { + config: Config; +} + +const Header = (props: Props): JSX.Element => { + const [dateTime, setDateTime] = useState(getDateTime()); + const [greeting, setGreeting] = useState(greeter()); + + useEffect(() => { + let dateTimeInterval: NodeJS.Timeout; + + dateTimeInterval = setInterval(() => { + setDateTime(getDateTime()); + setGreeting(greeter()); + }, 1000); + + return () => window.clearInterval(dateTimeInterval); + }, []); + + return ( +

+

{dateTime}

+ + Go to Settings + + +

{greeting}

+ +
+
+ ); +}; + +const mapStateToProps = (state: GlobalState) => { + return { + config: state.config.config, + }; +}; + +export default connect(mapStateToProps)(Header); diff --git a/client/src/components/Home/functions/dateTime.ts b/client/src/components/Home/Header/functions/getDateTime.ts similarity index 94% rename from client/src/components/Home/functions/dateTime.ts rename to client/src/components/Home/Header/functions/getDateTime.ts index ddcfc70..9f1d601 100644 --- a/client/src/components/Home/functions/dateTime.ts +++ b/client/src/components/Home/Header/functions/getDateTime.ts @@ -1,4 +1,4 @@ -export const dateTime = (): string => { +export const getDateTime = (): string => { const days = [ 'Sunday', 'Monday', diff --git a/client/src/components/Home/Header/functions/greeter.ts b/client/src/components/Home/Header/functions/greeter.ts new file mode 100644 index 0000000..93b32b4 --- /dev/null +++ b/client/src/components/Home/Header/functions/greeter.ts @@ -0,0 +1,17 @@ +export const greeter = (): string => { + const now = new Date().getHours(); + let msg: string; + + const greetingsSchemaRaw = + localStorage.getItem('greetingsSchema') || + 'Good evening!;Good afternoon!;Good morning!;Good night!'; + const greetingsSchema = greetingsSchemaRaw.split(';'); + + if (now >= 18) msg = greetingsSchema[0]; + else if (now >= 12) msg = greetingsSchema[1]; + else if (now >= 6) msg = greetingsSchema[2]; + else if (now >= 0) msg = greetingsSchema[3]; + else msg = 'Hello!'; + + return msg; +}; diff --git a/client/src/components/Home/Home.module.css b/client/src/components/Home/Home.module.css index 652ca22..f425184 100644 --- a/client/src/components/Home/Home.module.css +++ b/client/src/components/Home/Home.module.css @@ -1,24 +1,3 @@ -.Header h1 { - color: var(--color-primary); - font-weight: 700; - font-size: 4em; - display: inline-block; -} - -.Header p { - color: var(--color-primary); - font-weight: 300; - text-transform: uppercase; - height: 30px; -} - -.HeaderMain { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2.5rem; -} - .SettingsButton { width: 35px; height: 35px; @@ -40,21 +19,12 @@ opacity: 1; } -.SettingsLink { - visibility: visible; - color: var(--color-accent); -} - @media (min-width: 769px) { .SettingsButton { visibility: visible; } - - .SettingsLink { - visibility: hidden; - } } .HomeSpace { height: 20px; -} \ No newline at end of file +} diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 4a0adbe..017df9c 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -21,12 +21,8 @@ import classes from './Home.module.css'; // Components import AppGrid from '../Apps/AppGrid/AppGrid'; import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; -import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; import SearchBar from '../SearchBar/SearchBar'; - -// Functions -import { greeter } from './functions/greeter'; -import { dateTime } from './functions/dateTime'; +import Header from './Header/Header'; interface ComponentProps { getApps: Function; @@ -48,11 +44,6 @@ const Home = (props: ComponentProps): JSX.Element => { categoriesLoading, } = props; - const [header, setHeader] = useState({ - dateTime: dateTime(), - greeting: greeter(), - }); - // Local search query const [localSearch, setLocalSearch] = useState(null); const [appSearchResult, setAppSearchResult] = useState(null); @@ -74,23 +65,6 @@ const Home = (props: ComponentProps): JSX.Element => { } }, [getCategories]); - // Refresh greeter and time - useEffect(() => { - let interval: any; - - // Start interval only when hideHeader is false - if (!props.config.hideHeader) { - interval = setInterval(() => { - setHeader({ - dateTime: dateTime(), - greeting: greeter(), - }); - }, 1000); - } - - return () => clearInterval(interval); - }, []); - useEffect(() => { if (localSearch) { // Search through apps @@ -126,20 +100,7 @@ const Home = (props: ComponentProps): JSX.Element => {
)} - {!props.config.hideHeader ? ( -
-

{header.dateTime}

- - Go to Settings - - -

{header.greeting}

- -
-
- ) : ( -
- )} + {!props.config.hideHeader ?
:
} {!props.config.hideApps ? ( diff --git a/client/src/components/Home/functions/greeter.ts b/client/src/components/Home/functions/greeter.ts deleted file mode 100644 index 64cb2ea..0000000 --- a/client/src/components/Home/functions/greeter.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const greeter = (): string => { - const now = new Date().getHours(); - let msg: string; - - if (now >= 18) msg = 'Good evening!'; - else if (now >= 12) msg = 'Good afternoon!'; - else if (now >= 6) msg = 'Good morning!'; - else if (now >= 0) msg = 'Good night!'; - else msg = 'Hello!'; - - return msg; -} \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 6610b65..b076735 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -187,6 +187,21 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { + + + inputChangeHandler(e)} + /> + + Greetings must be separated with semicolon. Only 4 messages can be + used + + { onChange={(e) => inputChangeHandler(e)} /> + + {/* DATE FORMAT */} + + {/* PIN CATEGORIES */} + + {/* SORT TYPE */} + + {/* APPS OPPENING */} + + {/* BOOKMARKS OPPENING */} { + + {/* CUSTOM GREETINGS */} { used + + {/* CUSTOM DAYS */} + + + inputChangeHandler(e)} + /> + Names must be separated with semicolon + + + {/* CUSTOM MONTHS */} + + + inputChangeHandler(e)} + /> + Names must be separated with semicolon + + + {/* HIDE APPS */} + + {/* HIDE CATEGORIES */} { onChange={(e) => inputChangeHandler(e)} /> + + {/* USE DOCKER API */} + + {/* UNPIN DOCKER APPS */}