Compare commits

..

2 Commits

Author SHA1 Message Date
Christian Kellner
3d87aeb5f9 new chart system (#158)
* new chart system
2025-09-03 14:22:32 +02:00
weakmap@gmail.com
9774989eeb upgrade to react router 7 2025-09-02 20:18:37 +02:00
18 changed files with 899 additions and 670 deletions

View File

@@ -1,3 +0,0 @@
/ui/public
/db/
/conf/

View File

@@ -1,7 +1 @@
{
"interval": "60",
"port": 9998,
"workingHours": { "from": "", "to": "" },
"demoMode": false,
"analyticsEnabled": null
}
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":false}

View File

@@ -6,6 +6,10 @@ import react from 'eslint-plugin-react';
import babelParser from '@babel/eslint-parser';
export default [
{
files: ['**/*.{js,jsx,ts,tsx}'],
ignores: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/public/**', 'db/**', 'conf/**'],
},
js.configs.recommended,
prettier,
{
@@ -23,70 +27,34 @@ export default [
after: 'readonly',
it: 'readonly',
},
parserOptions: {
requireConfigFile: false,
},
},
plugins: {
react,
parserOptions: { requireConfigFile: false },
},
plugins: { react },
rules: {
eqeqeq: [2, 'allow-null'],
// Semantics / Performance impacting
strict: 0,
'no-redeclare': [2, { builtinGlobals: false }],
'class-methods-use-this': 'off',
// Style
indent: ['off', 2],
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
semi: ['error', 'always'],
'no-console': ['error', { allow: ['warn', 'error'] }],
// React
'jsx-quotes': ['error', 'prefer-double'],
'react/display-name': 'off',
'react/forbid-prop-types': 'off',
'react/jsx-closing-bracket-location': 'off',
'react/jsx-curly-spacing': 'off',
'react/jsx-handler-names': [
'off',
{
eventHandlerPrefix: 'handle',
eventHandlerPropPrefix: 'on',
},
],
'react/jsx-handler-names': ['off', { eventHandlerPrefix: 'handle', eventHandlerPropPrefix: 'on' }],
'react/jsx-indent-props': 'off',
'react/jsx-key': 'off',
'react/jsx-max-props-per-line': 'off',
'react/jsx-no-bind': [
'error',
{
ignoreRefs: true,
allowArrowFunctions: true,
allowBind: false,
},
],
'react/jsx-no-bind': ['error', { ignoreRefs: true, allowArrowFunctions: true, allowBind: false }],
'react/jsx-no-duplicate-props': ['error', { ignoreCase: true }],
'react/jsx-no-literals': 'off',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': [
'error',
{
allowAllCaps: true,
ignore: [],
},
],
'react/sort-prop-types': [
'off',
{
ignoreCase: true,
callbacksLast: false,
requiredFirst: false,
},
],
'react/jsx-pascal-case': ['error', { allowAllCaps: true, ignore: [] }],
'react/sort-prop-types': ['off', { ignoreCase: true, callbacksLast: false, requiredFirst: false }],
'react/jsx-sort-prop-types': 'off',
'react/jsx-sort-props': 'off',
'react/jsx-uses-react': 'error',
@@ -106,14 +74,7 @@ export default [
'react/require-render-return': 'error',
'react/self-closing-comp': 'warn',
'react/sort-comp': 'off',
'react/jsx-wrap-multilines': [
'warn',
{
declaration: true,
assignment: true,
return: true,
},
],
'react/jsx-wrap-multilines': ['warn', { declaration: true, assignment: true, return: true }],
'react/wrap-multilines': 'off',
'react/jsx-first-prop-new-line': 'off',
'react/jsx-equals-spacing': ['warn', 'never'],
@@ -126,20 +87,10 @@ export default [
'react/no-find-dom-node': 'warn',
'react/forbid-component-props': ['off', { forbid: [] }],
'react/no-danger-with-children': 'error',
'react/no-unused-prop-types': [
'warn',
{
customValidators: [],
skipShapeProps: true,
},
],
'react/no-unused-prop-types': ['warn', { customValidators: [], skipShapeProps: true }],
'react/style-prop-object': 'error',
'react/no-children-prop': 'warn',
},
settings: {
react: {
version: 'detect',
},
},
settings: { react: { version: 'detect' } },
},
];

View File

@@ -1,6 +1,6 @@
{
"name": "fredy",
"version": "11.4.4",
"version": "11.5.0",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
@@ -54,18 +54,18 @@
"Firefox ESR"
],
"dependencies": {
"@douyinfe/semi-ui": "2.85.0",
"@douyinfe/semi-ui": "2.86.0",
"@rematch/core": "2.2.0",
"@rematch/loading": "2.1.2",
"@sendgrid/mail": "8.1.5",
"@visactor/react-vchart": "^2.0.4",
"@visactor/vchart-semi-theme": "^1.12.2",
"@vitejs/plugin-react": "5.0.2",
"better-sqlite3": "^12.2.0",
"body-parser": "2.2.0",
"cheerio": "^1.1.2",
"cookie-session": "2.1.1",
"handlebars": "4.7.8",
"highcharts": "12.3.0",
"highcharts-react-official": "3.2.2",
"lodash": "4.17.21",
"lowdb": "7.0.1",
"markdown": "^0.5.0",
@@ -75,22 +75,22 @@
"node-mailjet": "6.0.9",
"p-throttle": "^8.0.0",
"package-up": "^5.0.0",
"puppeteer": "^24.17.1",
"puppeteer": "^24.18.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"query-string": "9.2.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "9.2.0",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "7.8.2",
"react-router-dom": "7.8.2",
"redux": "5.0.1",
"redux-thunk": "3.1.0",
"restana": "5.1.0",
"serve-static": "2.2.0",
"slack": "11.0.2",
"string-similarity": "^4.0.4",
"vite": "7.1.3",
"vite": "7.1.4",
"x-var": "^2.1.0"
},
"devDependencies": {
@@ -102,12 +102,12 @@
"eslint": "9.34.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
"esmock": "2.7.1",
"esmock": "2.7.2",
"history": "5.3.0",
"husky": "9.1.7",
"less": "4.4.1",
"lint-staged": "16.1.5",
"mocha": "11.7.1",
"lint-staged": "16.1.6",
"mocha": "11.7.2",
"nodemon": "^3.1.10",
"prettier": "3.6.2",
"redux-logger": "3.0.6"

View File

@@ -7,14 +7,13 @@ import JobMutation from './views/jobs/mutation/JobMutation';
import UserMutator from './views/user/mutation/UserMutator';
import JobInsight from './views/jobs/insights/JobInsight.jsx';
import { useDispatch, useSelector } from 'react-redux';
import { Switch, Redirect } from 'react-router-dom';
import { Routes, Route, Navigate } from 'react-router-dom';
import Logout from './components/logout/Logout';
import Logo from './components/logo/Logo';
import Menu from './components/menu/Menu';
import Login from './views/login/Login';
import Users from './views/user/Users';
import Jobs from './views/jobs/Jobs';
import { Route } from 'react-router';
import './App.less';
import TrackingModal from './components/tracking/TrackingModal.jsx';
@@ -49,10 +48,10 @@ export default function FredyApp() {
const isAdmin = () => currentUser != null && currentUser.isAdmin;
const login = () => (
<Switch>
<Route name="Login" path={'/login'} component={Login} />
<Redirect from="*" to={'/login'} />
</Switch>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
return loading ? null : needsLogin() ? (
@@ -77,34 +76,49 @@ export default function FredyApp() {
</>
)}
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
<Switch>
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} />
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} />
<Route name="Edit a Job" path={'/jobs/edit/:jobId'} component={JobMutation} />
<Route name="Insights into a Job" path={'/jobs/insights/:jobId'} component={JobInsight} />
<Route name="Job overview" path={'/jobs'} component={Jobs} />
<PermissionAwareRoute
name="Create new User"
<Routes>
<Route path="/403" element={<InsufficientPermission />} />
<Route path="/jobs/new" element={<JobMutation />} />
<Route path="/jobs/edit/:jobId" element={<JobMutation />} />
<Route path="/jobs/insights/:jobId" element={<JobInsight />} />
<Route path="/jobs" element={<Jobs />} />
{/* Permission-aware routes */}
<Route
path="/users/new"
component={<UserMutator />}
currentUser={currentUser}
element={
<PermissionAwareRoute currentUser={currentUser}>
<UserMutator />
</PermissionAwareRoute>
}
/>
<PermissionAwareRoute
name="Edit a user"
<Route
path="/users/edit/:userId"
component={<UserMutator />}
currentUser={currentUser}
element={
<PermissionAwareRoute currentUser={currentUser}>
<UserMutator />
</PermissionAwareRoute>
}
/>
<PermissionAwareRoute name="Users" path="/users" component={<Users />} currentUser={currentUser} />
<PermissionAwareRoute
name="General Settings"
<Route
path="/users"
element={
<PermissionAwareRoute currentUser={currentUser}>
<Users />
</PermissionAwareRoute>
}
/>
<Route
path="/generalSettings"
component={<GeneralSettings />}
currentUser={currentUser}
element={
<PermissionAwareRoute currentUser={currentUser}>
<GeneralSettings />
</PermissionAwareRoute>
}
/>
<Redirect from="/" to={'/jobs'} />
</Switch>
<Route path="/" element={<Navigate to="/jobs" replace />} />
</Routes>
</div>
</div>
);

View File

@@ -2,23 +2,24 @@ import React from 'react';
import { reduxStore } from './services/rematch/store';
import { HashRouter } from 'react-router-dom';
import { createHashHistory } from 'history';
import { Provider } from 'react-redux';
import { createRoot } from 'react-dom/client';
import en_US from '@douyinfe/semi-ui/lib/es/locale/source/en_US';
import { LocaleProvider } from '@douyinfe/semi-ui';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import App from './App';
import './Index.less';
const container = document.getElementById('fredy');
const root = createRoot(container);
const history = createHashHistory();
import App from './App';
import './Index.less';
initVChartSemiTheme({
defaultMode: 'dark',
});
root.render(
<Provider store={reduxStore}>
<HashRouter history={history}>
<HashRouter>
<LocaleProvider locale={en_US}>
<App />
</LocaleProvider>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { Tabs, TabPane } from '@douyinfe/semi-ui';
import { useLocation } from 'react-router';
import { useLocation } from 'react-router-dom';
import { IconUser, IconTerminal, IconSetting } from '@douyinfe/semi-icons';
import './Menu.less';
@@ -12,15 +12,10 @@ function parsePathName(name) {
}
const TopMenu = function TopMenu({ isAdmin }) {
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
return (
<Tabs
className="menu"
type="line"
activeKey={parsePathName(location.pathname)}
onTabClick={(key) => history.push(key)}
>
<Tabs className="menu" type="line" activeKey={parsePathName(location.pathname)} onTabClick={(key) => navigate(key)}>
<TabPane
itemKey="/jobs"
tab={

View File

@@ -1,21 +1,8 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Route } from 'react-router';
import { Navigate } from 'react-router-dom';
export default function PermissionAwareRoute({ currentUser, name, path, component }) {
/**
* Checks if given component should be rendered if current user has given permission enabled. If that's not the case,
* user is redirected to '/403'.
*
* @param permission
* @param component
* @param path
* @returns {*}
*/
const checkIfAdmin = (component, path) => {
return currentUser != null && currentUser.isAdmin ? component : <Redirect from={path} to="/403" />;
};
return <Route name={name} path={path} render={() => checkIfAdmin(component, path)} />;
export default function PermissionAwareRoute({ currentUser, children }) {
const isAdmin = currentUser != null && currentUser.isAdmin;
return isAdmin ? children : <Navigate to="/403" replace />;
}

View File

@@ -8,4 +8,4 @@ export function format(ts) {
second: 'numeric',
}).format(ts);
}
export const roundToNext5Minute = (ts) => Math.ceil(ts / (1000 * 60 * 5)) * (1000 * 60 * 5);
export const roundToHour = (ts) => Math.ceil(ts / (1000 * 60 * 60)) * (1000 * 60 * 60);

View File

@@ -3,7 +3,7 @@ import React from 'react';
import JobTable from '../../components/table/JobTable';
import { useSelector, useDispatch } from 'react-redux';
import { xhrDelete, xhrPut } from '../../services/xhr';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import ProcessingTimes from './ProcessingTimes';
import { Button, Toast } from '@douyinfe/semi-ui';
import { IconPlusCircle } from '@douyinfe/semi-icons';
@@ -12,7 +12,7 @@ import './Jobs.less';
export default function Jobs() {
const jobs = useSelector((state) => state.jobs.jobs);
const processingTimes = useSelector((state) => state.jobs.processingTimes);
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const onJobRemoval = async (jobId) => {
@@ -43,7 +43,7 @@ export default function Jobs() {
type="primary"
icon={<IconPlusCircle />}
className="jobs__newButton"
onClick={() => history.push('/jobs/new')}
onClick={() => navigate('/jobs/new')}
>
New Job
</Button>
@@ -53,8 +53,8 @@ export default function Jobs() {
jobs={jobs || []}
onJobRemoval={onJobRemoval}
onJobStatusChanged={onJobStatusChanged}
onJobInsight={(jobId) => history.push(`/jobs/insights/${jobId}`)}
onJobEdit={(jobId) => history.push(`/jobs/edit/${jobId}`)}
onJobInsight={(jobId) => navigate(`/jobs/insights/${jobId}`)}
onJobEdit={(jobId) => navigate(`/jobs/edit/${jobId}`)}
/>
</div>
);

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { roundToNext5Minute } from '../../../services/time/timeService';
import { roundToHour } from '../../../services/time/timeService';
import Headline from '../../../components/headline/Headline';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { useParams } from 'react-router-dom';
import Linechart from './Linechart';
const JobInsight = function JobInsight() {
@@ -20,27 +20,47 @@ const JobInsight = function JobInsight() {
const getData = () => {
const data = insights[params.jobId] || {};
const providers = Object.keys(data);
const result = [];
Object.keys(data).forEach((key) => {
const series = {
name: key[0].toUpperCase() + key.substring(1),
data: [],
};
const countsByProvider = {};
const allTimes = new Set();
const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : 'Unknown');
providers.forEach((key) => {
const providerName = cap(key);
const tmpTimeObj = {};
Object.values(data[key] || {}).forEach((listingTs) => {
const time = roundToNext5Minute(listingTs);
const time = roundToHour(listingTs);
tmpTimeObj[time] = tmpTimeObj[time] == null ? 1 : tmpTimeObj[time] + 1;
allTimes.add(time);
});
Object.keys(tmpTimeObj)
.sort()
.forEach((timeKey) => {
series.data.push([parseInt(timeKey), tmpTimeObj[timeKey]]);
countsByProvider[providerName] = tmpTimeObj;
});
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
const result = [];
providers.forEach((key) => {
const providerName = cap(key);
const bucket = countsByProvider[providerName] || {};
sortedTimes.forEach((t) => {
result.push({
listings: new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(new Date(parseInt(t))),
listingsNumber: bucket[t] || 0, // y value
provider: providerName, // series key
});
result.push(series);
});
});
return result;

View File

@@ -1,337 +1,49 @@
import React from 'react';
import Placeholder from '../../../components/placeholder/Placeholder';
import HighchartsReact from 'highcharts-react-official';
import Highcharts from 'highcharts/highcharts.src.js';
import { VChart } from '@visactor/react-vchart';
import './Linechart.less';
Highcharts.theme = {
colors: [
'#2b908f',
'#90ee7e',
'#f45b5b',
'#7798BF',
'#aaeeee',
'#ff0066',
'#eeaaee',
'#55BF3B',
'#DF5353',
'#7798BF',
'#aaeeee',
const commonSpec = {
type: 'line',
xField: 'listings',
yField: 'listingsNumber',
seriesField: 'provider',
legends: { visible: true },
line: {
style: {
lineWidth: 2,
},
},
point: {
visible: false,
},
axes: [
{
orient: 'bottom',
field: 'listings',
zero: false,
},
],
chart: {
backgroundColor: {
linearGradient: {
x1: 0,
y1: 0,
x2: 1,
y2: 1,
},
stops: [
[0, '#2a2a2b'],
[1, '#3e3e40'],
],
},
style: {
fontFamily: "'Unica One', sans-serif",
},
plotBorderColor: '#606063',
},
title: {
style: {
color: '#E0E0E3',
textTransform: 'uppercase',
fontSize: '20px',
},
},
subtitle: {
style: {
color: '#E0E0E3',
textTransform: 'uppercase',
},
},
xAxis: {
gridLineColor: '#707073',
labels: {
style: {
color: '#E0E0E3',
},
},
lineColor: '#707073',
minorGridLineColor: '#505053',
tickColor: '#707073',
title: {
style: {
color: '#A0A0A3',
},
},
},
yAxis: {
gridLineColor: '#707073',
labels: {
style: {
color: '#E0E0E3',
},
},
lineColor: '#707073',
minorGridLineColor: '#505053',
tickColor: '#707073',
tickWidth: 1,
title: {
style: {
color: '#A0A0A3',
},
},
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
style: {
color: '#F0F0F0',
},
},
plotOptions: {
series: {
dataLabels: {
color: '#F0F0F3',
style: {
fontSize: '13px',
},
},
marker: {
lineColor: '#333',
},
},
boxplot: {
fillColor: '#505053',
},
candlestick: {
lineColor: 'white',
},
errorbar: {
color: 'white',
},
},
legend: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
itemStyle: {
color: '#E0E0E3',
},
itemHoverStyle: {
color: '#FFF',
},
itemHiddenStyle: {
color: '#606063',
},
title: {
style: {
color: '#C0C0C0',
},
},
},
credits: {
style: {
color: '#666',
},
},
labels: {
style: {
color: '#707073',
},
},
drilldown: {
activeAxisLabelStyle: {
color: '#F0F0F3',
},
activeDataLabelStyle: {
color: '#F0F0F3',
},
},
navigation: {
buttonOptions: {
symbolStroke: '#DDDDDD',
theme: {
fill: '#505053',
},
},
},
// scroll charts
rangeSelector: {
buttonTheme: {
fill: '#505053',
stroke: '#000000',
style: {
color: '#CCC',
},
states: {
hover: {
fill: '#707073',
stroke: '#000000',
style: {
color: 'white',
},
},
select: {
fill: '#000003',
stroke: '#000000',
style: {
color: 'white',
},
},
},
},
inputBoxBorderColor: '#505053',
inputStyle: {
backgroundColor: '#333',
color: 'silver',
},
labelStyle: {
color: 'silver',
},
},
navigator: {
handles: {
backgroundColor: '#666',
borderColor: '#AAA',
},
outlineColor: '#CCC',
maskFill: 'rgba(255,255,255,0.1)',
series: {
color: '#7798BF',
lineColor: '#A6C7ED',
},
xAxis: {
gridLineColor: '#505053',
},
},
scrollbar: {
barBackgroundColor: '#808083',
barBorderColor: '#808083',
buttonArrowColor: '#CCC',
buttonBackgroundColor: '#606063',
buttonBorderColor: '#606063',
rifleColor: '#FFF',
trackBackgroundColor: '#404043',
trackBorderColor: '#404043',
},
};
// Apply the theme
Highcharts.setOptions(Highcharts.theme);
const defaultOptions = {
title: {
text: null,
},
legend: {
enabled: true,
},
xAxis: {
//most of the time (if not everytime), the x axis is time
type: 'datetime',
crosshair: {
snap: false,
},
},
yAxis: {
title: {
text: null,
},
//do not show float numbers
allowDecimals: false,
},
chart: {
type: 'line',
zoomType: 'x',
plotBackgroundColor: null,
plotBorderWidth: null,
},
exporting: {
enabled: false,
},
tooltip: {
shared: true,
formatter: null,
},
plotOptions: {
line: {
animation: false,
marker: {
enabled: false,
},
},
series: {
lineWidth: 1.5,
connectNulls: true,
marker: {
enabled: false,
},
},
},
series: [],
};
/**
* Usage of this chart:
* title: optional (show a title, if null, no title is shown)
* zoom: optional (if true, zooming in x axis is possible)
* legend: optional (show / hide the legend)
* series: mandatory (an array of data to be shown)
*
* <Linechart
* title="something"
* legend={true}
* timeframe={week/month/all} --> If this is not set, we assume the timeframe is 'all'
* //everything that is "subscribed" to this topic will receive this update
* highlightTopic="someTopic"
* height={"500px"}
* zoom={true}
* series={[
* {
* name: 'something',
* data: [x,y],
* dashStyle: (OPTIONAL) | solid / 'shortdot'
* }
* ]}
* />
*/
const Linechart = function Linechart({ title, series, height, isLoading = false }) {
const options = () => {
return {
...defaultOptions,
title: {
text: title,
},
time: {
useUTC: false,
},
legend: {
enabled: true,
},
series: series.map((series) => {
return {
...series,
};
}),
chart: {
type: 'line',
zoomType: 'x',
height: height || '400px',
},
};
};
const Linechart = function Linechart({ title, series, isLoading = false }) {
return (
<Placeholder ready={!isLoading} rows={6}>
{series == null || series.length === 0 ? (
<div className="linechart__no__data">No Data for selected timeframe :-/</div>
) : (
<HighchartsReact highcharts={Highcharts} options={options()} />
<VChart
spec={{
...commonSpec,
title: {
visible: true,
text: title,
},
data: { values: series },
}}
/>
)}
</Placeholder>
);

View File

@@ -7,8 +7,7 @@ import ProviderMutator from './components/provider/ProviderMutator';
import Headline from '../../../components/headline/Headline';
import { useDispatch, useSelector } from 'react-redux';
import { xhrPost } from '../../../services/xhr';
import { useHistory } from 'react-router-dom';
import { useParams } from 'react-router';
import { useNavigate, useParams } from 'react-router-dom';
import { Divider, Input, Switch, Button, TagInput, Toast } from '@douyinfe/semi-ui';
import './JobMutation.less';
import { SegmentPart } from '../../../components/segment/SegmentPart';
@@ -34,7 +33,7 @@ export default function JobMutator() {
const [blacklist, setBlacklist] = useState(defaultBlacklist);
const [notificationAdapterData, setNotificationAdapterData] = useState(defaultNotificationAdapter);
const [enabled, setEnabled] = useState(defaultEnabled);
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const isSavingEnabled = () => {
@@ -53,7 +52,7 @@ export default function JobMutator() {
});
await dispatch.jobs.getJobs();
Toast.success('Job successfully saved...');
history.push('/jobs');
navigate('/jobs');
} catch (Exception) {
console.error(Exception.json.message);
Toast.error(Exception.json != null ? Exception.json.message : Exception);
@@ -177,7 +176,7 @@ export default function JobMutator() {
<Switch className="jobMutation__spaceTop" onChange={(checked) => setEnabled(checked)} checked={enabled} />
</SegmentPart>
<Divider margin="1rem" />
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => history.push('/jobs')}>
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => navigate('/jobs')}>
Cancel
</Button>
<Button type="primary" icon={<IconPlusCircle />} disabled={!isSavingEnabled()} onClick={mutateJob}>

View File

@@ -30,7 +30,8 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
if (selectedProvider.baseUrl.indexOf(url.origin) === -1) {
return 'The url you have copied is not valid.';
}
} catch (Exception) {
/* eslint-disable no-unused-vars */
} catch (ignored) {
return 'The url you have copied is not valid.';
}
return null;

View File

@@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import cityBackground from '../../assets/city_background.jpg';
import Logo from '../../components/logo/Logo';
import { xhrPost } from '../../services/xhr';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { Input, Button, Banner, Toast } from '@douyinfe/semi-ui';
@@ -16,7 +16,7 @@ export default function Login() {
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState(null);
const demoMode = useSelector((state) => state.demoMode.demoMode || false);
const history = useHistory();
const navigate = useNavigate();
useEffect(() => {
async function init() {
@@ -38,7 +38,8 @@ export default function Login() {
username: username.trim(),
password,
});
} catch (Exception) {
/* eslint-disable no-unused-vars */
} catch (ignored) {
Toast.error('Login unsuccessful…');
return;
}
@@ -46,7 +47,7 @@ export default function Login() {
Toast.success('Login successful!');
await dispatch.user.getCurrentUser();
history.push('/jobs');
navigate('/jobs');
};
return (

View File

@@ -7,7 +7,7 @@ import { IconPlus } from '@douyinfe/semi-icons';
import { Button } from '@douyinfe/semi-ui';
import UserRemovalModal from './UserRemovalModal';
import { xhrDelete } from '../../services/xhr';
import { useHistory } from 'react-router';
import { useNavigate } from 'react-router-dom';
import './Users.less';
@@ -16,7 +16,7 @@ const Users = function Users() {
const [loading, setLoading] = React.useState(true);
const users = useSelector((state) => state.user.users);
const [userIdToBeRemoved, setUserIdToBeRemoved] = React.useState(null);
const history = useHistory();
const navigate = useNavigate();
React.useEffect(() => {
async function init() {
@@ -50,7 +50,7 @@ const Users = function Users() {
type="primary"
className="users__newButton"
icon={<IconPlus />}
onClick={() => history.push('/users/new')}
onClick={() => navigate('/users/new')}
>
Create new User
</Button>
@@ -58,7 +58,7 @@ const Users = function Users() {
<UserTable
user={users}
onUserEdit={(userId) => {
history.push(`/users/edit/${userId}`);
navigate(`/users/edit/${userId}`);
}}
onUserRemoval={(userId) => {
setUserIdToBeRemoved(userId);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { xhrGet, xhrPost } from '../../../services/xhr';
import { useHistory, useParams } from 'react-router';
import { useNavigate, useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Divider, Input, Switch, Button, Toast } from '@douyinfe/semi-ui';
import './UserMutator.less';
@@ -15,7 +15,7 @@ const UserMutator = function UserMutator() {
const [password2, setPassword2] = React.useState('');
const [isAdmin, setIsAdmin] = React.useState(false);
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
React.useEffect(() => {
@@ -50,7 +50,7 @@ const UserMutator = function UserMutator() {
});
await dispatch.user.getUsers();
Toast.success('User successfully saved...');
history.push('/users');
navigate('/users');
} catch (error) {
console.error(error);
Toast.error(error.json.message);
@@ -98,7 +98,7 @@ const UserMutator = function UserMutator() {
<Switch checked={isAdmin} onChange={(checked) => setIsAdmin(checked)} />
</SegmentPart>
<Divider margin="1rem" />
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => history.push('/users')}>
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => navigate('/users')}>
Cancel
</Button>
<Button type="primary" icon={<IconPlusCircle />} onClick={saveUser}>

897
yarn.lock

File diff suppressed because it is too large Load Diff