Local search for apps

This commit is contained in:
unknown 2021-09-06 12:24:01 +02:00
parent 8521995758
commit 6ae6c58f4c
14 changed files with 297 additions and 4751 deletions

View file

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

View file

@ -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 = (
<div className={classes.AppGrid}>
{props.apps.map((app: App): JSX.Element => {
return <AppCard
key={app.id}
app={app}
/>
return <AppCard key={app.id} app={app} />;
})}
</div>
)
);
} else {
if (props.totalApps) {
apps = (
<p className={classes.AppsMessage}>There are no pinned applications. You can pin them from the <Link to='/applications'>/applications</Link> menu</p>
);
if (props.searching) {
apps = (
<p className={classes.AppsMessage}>
No apps match your search criteria
</p>
);
} else {
apps = (
<p className={classes.AppsMessage}>
There are no pinned applications. You can pin them from the{' '}
<Link to="/applications">/applications</Link> menu
</p>
);
}
} else {
apps = (
<p className={classes.AppsMessage}>You don't have any applications. You can add a new one from <Link to='/applications'>/applications</Link> menu</p>
<p className={classes.AppsMessage}>
You don't have any applications. You can add a new one from{' '}
<Link to="/applications">/applications</Link> menu
</p>
);
}
}
return apps;
}
};
export default AppGrid;
export default AppGrid;

View file

@ -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 (
<Container>
<Modal isOpen={modalIsOpen} setIsOpen={setModalIsOpen}>
{!isInUpdate
? <AppForm modalHandler={toggleModal} />
: <AppForm modalHandler={toggleModal} app={appInUpdate} />
}
{!isInUpdate ? (
<AppForm modalHandler={toggleModal} />
) : (
<AppForm modalHandler={toggleModal} app={appInUpdate} />
)}
</Modal>
<Headline
title='All Applications'
subtitle={(<Link to='/'>Go back</Link>)}
title="All Applications"
subtitle={<Link to="/">Go back</Link>}
/>
<div className={classes.ActionsContainer}>
<ActionButton
name='Add'
icon='mdiPlusBox'
handler={toggleModal}
/>
<ActionButton
name='Edit'
icon='mdiPencil'
handler={toggleEdit}
/>
<ActionButton name="Add" icon="mdiPlusBox" handler={toggleModal} />
<ActionButton name="Edit" icon="mdiPencil" handler={toggleEdit} />
</div>
<div className={classes.Apps}>
{loading
? <Spinner />
: (!isInEdit
? <AppGrid apps={apps} />
: <AppTable updateAppHandler={toggleUpdate} />)
}
{loading ? (
<Spinner />
) : !isInEdit ? (
<AppGrid apps={apps} searching />
) : (
<AppTable updateAppHandler={toggleUpdate} />
)}
</div>
</Container>
)
}
);
};
const mapStateToProps = (state: GlobalState) => {
return {
apps: state.app.apps,
loading: state.app.loading
}
}
loading: state.app.loading,
};
};
export default connect(mapStateToProps, { getApps })(Apps);
export default connect(mapStateToProps, { getApps })(Apps);

View file

@ -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 | string>(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 (
<Container>
{searchConfig('hideSearch', 0) !== 1
? <SearchBar />
: <div></div>
}
{searchConfig('hideSearch', 0) !== 1 ? (
<SearchBar setLocalSearch={setLocalSearch} />
) : (
<div></div>
)}
{searchConfig('hideHeader', 0) !== 1
? (
<header className={classes.Header}>
<p>{header.dateTime}</p>
<Link to='/settings' className={classes.SettingsLink}>Go to Settings</Link>
<span className={classes.HeaderMain}>
<h1>{header.greeting}</h1>
<WeatherWidget />
</span>
</header>
)
: <div></div>
}
{searchConfig('hideApps', 0) !== 1
? (<Fragment>
<SectionHeadline title='Applications' link='/applications' />
{appsLoading
? <Spinner />
: <AppGrid
apps={apps.filter((app: App) => app.isPinned)}
totalApps={apps.length}
/>
}
<div className={classes.HomeSpace}></div>
</Fragment>)
: <div></div>
}
{searchConfig('hideHeader', 0) !== 1 ? (
<header className={classes.Header}>
<p>{header.dateTime}</p>
<Link to="/settings" className={classes.SettingsLink}>
Go to Settings
</Link>
<span className={classes.HeaderMain}>
<h1>{header.greeting}</h1>
<WeatherWidget />
</span>
</header>
) : (
<div></div>
)}
{searchConfig('hideCategories', 0) !== 1
? (<Fragment>
<SectionHeadline title='Bookmarks' link='/bookmarks' />
{categoriesLoading
? <Spinner />
: <BookmarkGrid
categories={categories.filter((category: Category) => category.isPinned)}
totalCategories={categories.length}
/>
}
</Fragment>)
: <div></div>
}
{searchConfig('hideApps', 0) !== 1 ? (
<Fragment>
<SectionHeadline title="Applications" link="/applications" />
{appsLoading ? (
<Spinner />
) : (
<AppGrid
apps={
!localSearch
? apps.filter(({ isPinned }) => isPinned)
: apps.filter(({ name }) =>
new RegExp(localSearch, 'i').test(name)
)
}
totalApps={apps.length}
searching={!!localSearch}
/>
)}
<div className={classes.HomeSpace}></div>
</Fragment>
) : (
<div></div>
)}
<Link to='/settings' className={classes.SettingsButton}>
<Icon icon='mdiCog' color='var(--color-background)' />
{searchConfig('hideCategories', 0) !== 1 ? (
<Fragment>
<SectionHeadline title="Bookmarks" link="/bookmarks" />
{categoriesLoading ? (
<Spinner />
) : (
<BookmarkGrid
categories={categories.filter(
(category: Category) => category.isPinned
)}
totalCategories={categories.length}
/>
)}
</Fragment>
) : (
<div></div>
)}
<Link to="/settings" className={classes.SettingsButton}>
<Icon icon="mdiCog" color="var(--color-background)" />
</Link>
</Container>
)
}
);
};
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);
export default connect(mapStateToProps, { getApps, getCategories })(Home);

View file

@ -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<HTMLInputElement>(document.createElement('input'));
useEffect(() => {
inputRef.current.focus();
}, [])
}, []);
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
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 (
<input
ref={inputRef}
type='text'
type="text"
className={classes.SearchBar}
onKeyDown={(e) => searchHandler(e)}
/>
)
}
);
};
export default connect(null, { createNotification })(SearchBar);
export default connect(null, { createNotification })(SearchBar);

View file

@ -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 (
<form onSubmit={e => formSubmitHandler(e)}>
<form onSubmit={(e) => formSubmitHandler(e)}>
{/* OTHER OPTIONS */}
<h2 className={classes.SettingsSection}>Miscellaneous</h2>
<InputGroup>
<label htmlFor='customTitle'>Custom page title</label>
<label htmlFor="customTitle">Custom page title</label>
<input
type='text'
id='customTitle'
name='customTitle'
placeholder='Flame'
type="text"
id="customTitle"
name="customTitle"
placeholder="Flame"
value={formData.customTitle}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
{/* BEAHVIOR OPTIONS */}
<h2 className={classes.SettingsSection}>App Behavior</h2>
<InputGroup>
<label htmlFor='pinAppsByDefault'>
<label htmlFor="pinAppsByDefault">
Pin new applications by default
</label>
<select
id='pinAppsByDefault'
name='pinAppsByDefault'
id="pinAppsByDefault"
name="pinAppsByDefault"
value={formData.pinAppsByDefault}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='pinCategoriesByDefault'>
<label htmlFor="pinCategoriesByDefault">
Pin new categories by default
</label>
<select
id='pinCategoriesByDefault'
name='pinCategoriesByDefault'
id="pinCategoriesByDefault"
name="pinCategoriesByDefault"
value={formData.pinCategoriesByDefault}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='useOrdering'>Sorting type</label>
<label htmlFor="useOrdering">Sorting type</label>
<select
id='useOrdering'
name='useOrdering'
id="useOrdering"
name="useOrdering"
value={formData.useOrdering}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
>
<option value='createdAt'>By creation date</option>
<option value='name'>Alphabetical order</option>
<option value='orderId'>Custom order</option>
<option value="createdAt">By creation date</option>
<option value="name">Alphabetical order</option>
<option value="orderId">Custom order</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='defaultSearchProvider'>Default Search Provider</label>
<label htmlFor="defaultSearchProvider">Default Search Provider</label>
<select
id='defaultSearchProvider'
name='defaultSearchProvider'
id="defaultSearchProvider"
name="defaultSearchProvider"
value={formData.defaultSearchProvider}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
>
{queries.map((query: Query) => (
<option value={query.prefix}>{query.name}</option>
{queries.map((query: Query, idx) => (
<option key={idx} value={query.prefix}>
{query.name}
</option>
))}
</select>
</InputGroup>
<InputGroup>
<label htmlFor='searchSameTab'>
<label htmlFor="searchSameTab">
Open search results in the same tab
</label>
<select
id='searchSameTab'
name='searchSameTab'
id="searchSameTab"
name="searchSameTab"
value={formData.searchSameTab}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='appsSameTab'>Open applications in the same tab</label>
<label htmlFor="appsSameTab">Open applications in the same tab</label>
<select
id='appsSameTab'
name='appsSameTab'
id="appsSameTab"
name="appsSameTab"
value={formData.appsSameTab}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='bookmarksSameTab'>Open bookmarks in the same tab</label>
<label htmlFor="bookmarksSameTab">Open bookmarks in the same tab</label>
<select
id='bookmarksSameTab'
name='bookmarksSameTab'
id="bookmarksSameTab"
name="bookmarksSameTab"
value={formData.bookmarksSameTab}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
@ -223,48 +225,48 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* MODULES OPTIONS */}
<h2 className={classes.SettingsSection}>Modules</h2>
<InputGroup>
<label htmlFor='hideSearch'>Hide search bar</label>
<label htmlFor="hideSearch">Hide search bar</label>
<select
id='hideSearch'
name='hideSearch'
id="hideSearch"
name="hideSearch"
value={formData.hideSearch}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='hideHeader'>Hide greeting and date</label>
<label htmlFor="hideHeader">Hide greeting and date</label>
<select
id='hideHeader'
name='hideHeader'
id="hideHeader"
name="hideHeader"
value={formData.hideHeader}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='hideApps'>Hide applications</label>
<label htmlFor="hideApps">Hide applications</label>
<select
id='hideApps'
name='hideApps'
id="hideApps"
name="hideApps"
value={formData.hideApps}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='hideCategories'>Hide categories</label>
<label htmlFor="hideCategories">Hide categories</label>
<select
id='hideCategories'
name='hideCategories'
id="hideCategories"
name="hideCategories"
value={formData.hideCategories}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
@ -274,26 +276,26 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* DOCKER SETTINGS */}
<h2 className={classes.SettingsSection}>Docker</h2>
<InputGroup>
<label htmlFor='dockerApps'>Use Docker API</label>
<label htmlFor="dockerApps">Use Docker API</label>
<select
id='dockerApps'
name='dockerApps'
id="dockerApps"
name="dockerApps"
value={formData.dockerApps}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='unpinStoppedApps'>
<label htmlFor="unpinStoppedApps">
Unpin stopped containers / other apps
</label>
<select
id='unpinStoppedApps'
name='unpinStoppedApps'
id="unpinStoppedApps"
name="unpinStoppedApps"
value={formData.unpinStoppedApps}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
@ -303,12 +305,12 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* KUBERNETES SETTINGS */}
<h2 className={classes.SettingsSection}>Kubernetes</h2>
<InputGroup>
<label htmlFor='kubernetesApps'>Use Kubernetes Ingress API</label>
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
<select
id='kubernetesApps'
name='kubernetesApps'
id="kubernetesApps"
name="kubernetesApps"
value={formData.kubernetesApps}
onChange={e => inputChangeHandler(e, true)}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
@ -321,7 +323,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
const mapStateToProps = (state: GlobalState) => {
return {
loading: state.config.loading
loading: state.config.loading,
};
};
@ -329,7 +331,7 @@ const actions = {
createNotification,
updateConfig,
sortApps,
sortCategories
sortCategories,
};
export default connect(mapStateToProps, actions)(OtherSettings);

View file

@ -0,0 +1,5 @@
export interface SearchResult {
isLocal: boolean;
prefix: null | string;
query: string;
}

View file

@ -8,4 +8,5 @@ export * from './Category';
export * from './Notification';
export * from './Config';
export * from './Forms';
export * from './Query';
export * from './Query';
export * from './SearchResult';

View file

@ -1,26 +1,45 @@
import { queries } from './searchQueries.json';
import { Query } from '../interfaces';
import { Query, SearchResult } from '../interfaces';
import { searchConfig } from '.';
export const searchParser = (searchQuery: string): boolean => {
export const searchParser = (searchQuery: string): SearchResult => {
const result: SearchResult = {
isLocal: false,
prefix: null,
query: '',
};
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
const prefix = splitQuery ? splitQuery[1] : searchConfig('defaultSearchProvider', 'd');
const search = splitQuery ? encodeURIComponent(splitQuery[2]) : encodeURIComponent(searchQuery);
const prefix = splitQuery
? splitQuery[1]
: searchConfig('defaultSearchProvider', 'l');
const search = splitQuery
? encodeURIComponent(splitQuery[2])
: encodeURIComponent(searchQuery);
const query = queries.find((q: Query) => q.prefix === prefix);
if (query) {
const sameTab = searchConfig('searchSameTab', false);
result.prefix = query.prefix;
result.query = search;
if (sameTab) {
document.location.replace(`${query.template}${search}`);
if (prefix === 'l') {
result.isLocal = true;
} else {
window.open(`${query.template}${search}`);
const sameTab = searchConfig('searchSameTab', false);
if (sameTab) {
document.location.replace(`${query.template}${search}`);
} else {
window.open(`${query.template}${search}`);
}
}
return true;
return result;
}
return false;
}
return result;
};

View file

@ -25,6 +25,11 @@
"prefix": "im",
"template": "https://www.imdb.com/find?q="
},
{
"name": "Local search",
"prefix": "l",
"template": "#"
},
{
"name": "Reddit",
"prefix": "r",

4536
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,13 @@
"main": "index.js",
"scripts": {
"start": "node server.js",
"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",
"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\""
"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",
"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\""
},
"author": "",
"license": "ISC",

View file

@ -9,27 +9,27 @@ const initConfig = async () => {
const configPairs = await Config.findAll({
where: {
key: {
[Op.or]: config.map(pair => pair.key)
}
}
})
[Op.or]: config.map((pair) => pair.key),
},
},
});
// Get key from each pair
const configKeys = configPairs.map((pair) => pair.key);
// Create missing pairs
config.forEach(async ({ key, value}) => {
config.forEach(async ({ key, value }) => {
if (!configKeys.includes(key)) {
await Config.create({
key,
value,
valueType: typeof value
})
valueType: typeof value,
});
}
})
});
logger.log('Initial config created');
return;
}
};
module.exports = initConfig;
module.exports = initConfig;

View file

@ -62,7 +62,7 @@
},
{
"key": "defaultSearchProvider",
"value": "d"
"value": "l"
},
{
"key": "dockerApps",