mirror of
https://github.com/pawelmalak/flame.git
synced 2025-07-19 11:39:36 +02:00
commit
3dd255f359
15 changed files with 155 additions and 49 deletions
2
.env
2
.env
|
@ -1,3 +1,3 @@
|
||||||
PORT=5005
|
PORT=5005
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
VERSION=1.7.1
|
VERSION=1.7.2
|
|
@ -1,3 +1,9 @@
|
||||||
|
### 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))
|
||||||
|
- Added option to disable search bar autofocus ([#127](https://github.com/pawelmalak/flame/issues/127))
|
||||||
|
|
||||||
### v1.7.1 (2021-10-22)
|
### v1.7.1 (2021-10-22)
|
||||||
- Fixed search action not being triggered by Numpad Enter
|
- Fixed search action not being triggered by Numpad Enter
|
||||||
- Added option to change date formatting ([#92](https://github.com/pawelmalak/flame/issues/92))
|
- Added option to change date formatting ([#92](https://github.com/pawelmalak/flame/issues/92))
|
||||||
|
|
10
DEV_GUIDELINES.md
Normal file
10
DEV_GUIDELINES.md
Normal file
|
@ -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
|
|
@ -1 +1 @@
|
||||||
REACT_APP_VERSION=1.7.1
|
REACT_APP_VERSION=1.7.2
|
|
@ -55,17 +55,21 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
// Local search query
|
// Local search query
|
||||||
const [localSearch, setLocalSearch] = useState<null | string>(null);
|
const [localSearch, setLocalSearch] = useState<null | string>(null);
|
||||||
|
const [appSearchResult, setAppSearchResult] = useState<null | App[]>(null);
|
||||||
|
const [bookmarkSearchResult, setBookmarkSearchResult] = useState<
|
||||||
|
null | Category[]
|
||||||
|
>(null);
|
||||||
|
|
||||||
// Load applications
|
// Load applications
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apps.length === 0) {
|
if (!apps.length) {
|
||||||
getApps();
|
getApps();
|
||||||
}
|
}
|
||||||
}, [getApps]);
|
}, [getApps]);
|
||||||
|
|
||||||
// Load bookmark categories
|
// Load bookmark categories
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (categories.length === 0) {
|
if (!categories.length) {
|
||||||
getCategories();
|
getCategories();
|
||||||
}
|
}
|
||||||
}, [getCategories]);
|
}, [getCategories]);
|
||||||
|
@ -87,22 +91,37 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Search bookmarks
|
useEffect(() => {
|
||||||
const searchBookmarks = (query: string): Category[] => {
|
if (localSearch) {
|
||||||
|
// Search through apps
|
||||||
|
setAppSearchResult([
|
||||||
|
...apps.filter(({ name }) => new RegExp(localSearch, 'i').test(name)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Search through bookmarks
|
||||||
const category = { ...categories[0] };
|
const category = { ...categories[0] };
|
||||||
|
|
||||||
category.name = 'Search Results';
|
category.name = 'Search Results';
|
||||||
category.bookmarks = categories
|
category.bookmarks = categories
|
||||||
.map(({ bookmarks }) => bookmarks)
|
.map(({ bookmarks }) => bookmarks)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(({ name }) => new RegExp(query, 'i').test(name));
|
.filter(({ name }) => new RegExp(localSearch, 'i').test(name));
|
||||||
|
|
||||||
return [category];
|
setBookmarkSearchResult([category]);
|
||||||
};
|
} else {
|
||||||
|
setAppSearchResult(null);
|
||||||
|
setBookmarkSearchResult(null);
|
||||||
|
}
|
||||||
|
}, [localSearch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{!props.config.hideSearch ? (
|
{!props.config.hideSearch ? (
|
||||||
<SearchBar setLocalSearch={setLocalSearch} />
|
<SearchBar
|
||||||
|
setLocalSearch={setLocalSearch}
|
||||||
|
appSearchResult={appSearchResult}
|
||||||
|
bookmarkSearchResult={bookmarkSearchResult}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
)}
|
)}
|
||||||
|
@ -130,11 +149,9 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
) : (
|
) : (
|
||||||
<AppGrid
|
<AppGrid
|
||||||
apps={
|
apps={
|
||||||
!localSearch
|
!appSearchResult
|
||||||
? apps.filter(({ isPinned }) => isPinned)
|
? apps.filter(({ isPinned }) => isPinned)
|
||||||
: apps.filter(({ name }) =>
|
: appSearchResult
|
||||||
new RegExp(localSearch, 'i').test(name)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
totalApps={apps.length}
|
totalApps={apps.length}
|
||||||
searching={!!localSearch}
|
searching={!!localSearch}
|
||||||
|
@ -154,9 +171,9 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||||
) : (
|
) : (
|
||||||
<BookmarkGrid
|
<BookmarkGrid
|
||||||
categories={
|
categories={
|
||||||
!localSearch
|
!bookmarkSearchResult
|
||||||
? categories.filter(({ isPinned }) => isPinned)
|
? categories.filter(({ isPinned }) => isPinned)
|
||||||
: searchBookmarks(localSearch)
|
: bookmarkSearchResult
|
||||||
}
|
}
|
||||||
totalCategories={categories.length}
|
totalCategories={categories.length}
|
||||||
searching={!!localSearch}
|
searching={!!localSearch}
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { connect } from 'react-redux';
|
||||||
import { createNotification } from '../../store/actions';
|
import { createNotification } from '../../store/actions';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { NewNotification } from '../../interfaces';
|
import {
|
||||||
|
App,
|
||||||
|
Category,
|
||||||
|
Config,
|
||||||
|
GlobalState,
|
||||||
|
NewNotification,
|
||||||
|
} from '../../interfaces';
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
import classes from './SearchBar.module.css';
|
import classes from './SearchBar.module.css';
|
||||||
|
@ -16,15 +22,44 @@ import { searchParser, urlParser, redirectUrl } from '../../utility';
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
setLocalSearch: (query: string) => void;
|
setLocalSearch: (query: string) => void;
|
||||||
|
appSearchResult: App[] | null;
|
||||||
|
bookmarkSearchResult: Category[] | null;
|
||||||
|
config: Config;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBar = (props: ComponentProps): JSX.Element => {
|
const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||||
const { setLocalSearch, createNotification } = props;
|
const {
|
||||||
|
setLocalSearch,
|
||||||
|
createNotification,
|
||||||
|
config,
|
||||||
|
loading,
|
||||||
|
appSearchResult,
|
||||||
|
bookmarkSearchResult,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
||||||
|
|
||||||
|
// Search bar autofocus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!loading && !config.disableAutofocus) {
|
||||||
inputRef.current.focus();
|
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 = () => {
|
const clearSearch = () => {
|
||||||
|
@ -53,8 +88,12 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||||
const url = urlParser(inputRef.current.value)[1];
|
const url = urlParser(inputRef.current.value)[1];
|
||||||
redirectUrl(url, sameTab);
|
redirectUrl(url, sameTab);
|
||||||
} else if (isLocal) {
|
} else if (isLocal) {
|
||||||
// Local query -> filter apps and bookmarks
|
// Local query -> redirect if at least 1 result found
|
||||||
setLocalSearch(search);
|
if (appSearchResult?.length) {
|
||||||
|
redirectUrl(appSearchResult[0].url, sameTab);
|
||||||
|
} else if (bookmarkSearchResult?.length) {
|
||||||
|
redirectUrl(bookmarkSearchResult[0].bookmarks[0].url, sameTab);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Valid query -> redirect to search results
|
// Valid query -> redirect to search results
|
||||||
const url = `${query.template}${search}`;
|
const url = `${query.template}${search}`;
|
||||||
|
@ -78,4 +117,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);
|
||||||
|
|
|
@ -121,6 +121,18 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||||
<option value={0}>False</option>
|
<option value={0}>False</option>
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="disableAutofocus">Disable search bar autofocus</label>
|
||||||
|
<select
|
||||||
|
id="disableAutofocus"
|
||||||
|
name="disableAutofocus"
|
||||||
|
value={formData.disableAutofocus ? 1 : 0}
|
||||||
|
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
<Button>Save changes</Button>
|
<Button>Save changes</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,5 @@ export interface Config {
|
||||||
kubernetesApps: boolean;
|
kubernetesApps: boolean;
|
||||||
unpinStoppedApps: boolean;
|
unpinStoppedApps: boolean;
|
||||||
useAmericanDate: boolean;
|
useAmericanDate: boolean;
|
||||||
|
disableAutofocus: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export interface SearchForm {
|
||||||
hideSearch: boolean;
|
hideSearch: boolean;
|
||||||
defaultSearchProvider: string;
|
defaultSearchProvider: string;
|
||||||
searchSameTab: boolean;
|
searchSameTab: boolean;
|
||||||
|
disableAutofocus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OtherSettingsForm {
|
export interface OtherSettingsForm {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { urlParser } from '.';
|
||||||
|
|
||||||
export const redirectUrl = (url: string, sameTab: boolean) => {
|
export const redirectUrl = (url: string, sameTab: boolean) => {
|
||||||
|
const parsedUrl = urlParser(url)[1];
|
||||||
|
|
||||||
if (sameTab) {
|
if (sameTab) {
|
||||||
document.location.replace(url);
|
document.location.replace(parsedUrl);
|
||||||
} else {
|
} else {
|
||||||
window.open(url);
|
window.open(parsedUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,4 +22,5 @@ export const configTemplate: Config = {
|
||||||
kubernetesApps: false,
|
kubernetesApps: false,
|
||||||
unpinStoppedApps: false,
|
unpinStoppedApps: false,
|
||||||
useAmericanDate: false,
|
useAmericanDate: false,
|
||||||
|
disableAutofocus: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,4 +28,5 @@ export const searchSettingsTemplate: SearchForm = {
|
||||||
hideSearch: false,
|
hideSearch: false,
|
||||||
searchSameTab: false,
|
searchSameTab: false,
|
||||||
defaultSearchProvider: 'l',
|
defaultSearchProvider: 'l',
|
||||||
|
disableAutofocus: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,14 +5,6 @@ const loadConfig = require('./loadConfig');
|
||||||
const getExternalWeather = async () => {
|
const getExternalWeather = async () => {
|
||||||
const { WEATHER_API_KEY: secret, lat, long } = await loadConfig();
|
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
|
// Fetch data from external API
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(
|
const res = await axios.get(
|
||||||
|
|
|
@ -18,5 +18,7 @@
|
||||||
"dockerApps": false,
|
"dockerApps": false,
|
||||||
"dockerHost": "localhost",
|
"dockerHost": "localhost",
|
||||||
"kubernetesApps": false,
|
"kubernetesApps": false,
|
||||||
"unpinStoppedApps": false
|
"unpinStoppedApps": false,
|
||||||
|
"useAmericanDate": false,
|
||||||
|
"disableAutofocus": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,20 +3,33 @@ const getExternalWeather = require('./getExternalWeather');
|
||||||
const clearWeatherData = require('./clearWeatherData');
|
const clearWeatherData = require('./clearWeatherData');
|
||||||
const Sockets = require('../Sockets');
|
const Sockets = require('../Sockets');
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
|
const loadConfig = require('./loadConfig');
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
// Update weather data every 15 minutes
|
// Update weather data every 15 minutes
|
||||||
const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => {
|
const weatherJob = schedule.scheduleJob(
|
||||||
|
'updateWeather',
|
||||||
|
'0 */15 * * * *',
|
||||||
|
async () => {
|
||||||
|
const { WEATHER_API_KEY: secret } = await loadConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const weatherData = await getExternalWeather();
|
const weatherData = await getExternalWeather();
|
||||||
logger.log('Weather updated');
|
logger.log('Weather updated');
|
||||||
Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData));
|
Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (secret) {
|
||||||
logger.log(err.message, 'ERROR');
|
logger.log(err.message, 'ERROR');
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Clear old weather data every 4 hours
|
// Clear old weather data every 4 hours
|
||||||
const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 5 */4 * * *', async () => {
|
const weatherCleanerJob = schedule.scheduleJob(
|
||||||
|
'clearWeather',
|
||||||
|
'0 5 */4 * * *',
|
||||||
|
async () => {
|
||||||
clearWeatherData();
|
clearWeatherData();
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue