mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Ui-Redesign (#203)
* new ui design * improving ui design * adding new screenshots * upgrade dependencies
This commit is contained in:
committed by
GitHub
parent
412e24b1e3
commit
b6755497e4
@@ -107,9 +107,9 @@ yarn run start:frontend # in another terminal
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
| Job Configuration | Job Analytics | Job Overview |
|
||||
|-------------------|--------------|--------------|
|
||||
|  |  |  |
|
||||
| Fredy Main Overview | Job Configuration | Found Listings |
|
||||
|--------------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 197 KiB |
BIN
doc/screenshot2.png
Normal file
BIN
doc/screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
BIN
doc/screenshot3.png
Normal file
BIN
doc/screenshot3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 372 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 323 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB |
@@ -8,7 +8,14 @@ const versionRouter = service.newRouter();
|
||||
|
||||
versionRouter.get('/', async (req, res) => {
|
||||
const versionPayload = await getCurrentVersionFromGithub();
|
||||
res.body = versionPayload == null ? { newVersion: false } : versionPayload;
|
||||
const localFredyVersion = await getPackageVersion();
|
||||
res.body =
|
||||
versionPayload == null
|
||||
? {
|
||||
newVersion: false,
|
||||
localFredyVersion,
|
||||
}
|
||||
: versionPayload;
|
||||
res.send();
|
||||
});
|
||||
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fredy",
|
||||
"version": "12.3.2",
|
||||
"version": "14.0.0",
|
||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
@@ -62,7 +62,7 @@
|
||||
"@visactor/react-vchart": "^2.0.5",
|
||||
"@visactor/vchart": "^2.0.5",
|
||||
"@visactor/vchart-semi-theme": "^1.12.2",
|
||||
"@vitejs/plugin-react": "5.0.3",
|
||||
"@vitejs/plugin-react": "5.0.4",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"body-parser": "2.2.0",
|
||||
"cheerio": "^1.1.2",
|
||||
@@ -82,8 +82,8 @@
|
||||
"query-string": "9.3.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.9.2",
|
||||
"react-router-dom": "7.9.2",
|
||||
"react-router": "7.9.3",
|
||||
"react-router-dom": "7.9.3",
|
||||
"restana": "5.1.0",
|
||||
"semver": "^7.7.2",
|
||||
"serve-static": "2.2.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"@babel/eslint-parser": "7.28.4",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"chai": "6.0.1",
|
||||
"chai": "6.2.0",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
@@ -105,7 +105,7 @@
|
||||
"history": "5.3.0",
|
||||
"husky": "9.1.7",
|
||||
"less": "4.4.1",
|
||||
"lint-staged": "16.2.1",
|
||||
"lint-staged": "16.2.3",
|
||||
"mocha": "11.7.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "3.6.2"
|
||||
|
||||
146
ui/src/App.jsx
146
ui/src/App.jsx
@@ -8,18 +8,19 @@ import UserMutator from './views/user/mutation/UserMutator';
|
||||
import JobInsight from './views/jobs/insights/JobInsight.jsx';
|
||||
import { useActions, useSelector } from './services/state/store';
|
||||
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 './App.less';
|
||||
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
||||
import { Banner } from '@douyinfe/semi-ui';
|
||||
import { Banner, Divider } from '@douyinfe/semi-ui';
|
||||
import VersionBanner from './components/version/VersionBanner.jsx';
|
||||
import Listings from './views/listings/Listings.jsx';
|
||||
import Navigation from './components/navigation/Navigation.jsx';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import FredyFooter from './components/footer/FredyFooter.jsx';
|
||||
import ProcessingTimes from './views/jobs/ProcessingTimes.jsx';
|
||||
|
||||
export default function FredyApp() {
|
||||
const actions = useActions();
|
||||
@@ -27,6 +28,7 @@ export default function FredyApp() {
|
||||
const currentUser = useSelector((state) => state.user.currentUser);
|
||||
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
const processingTimes = useSelector((state) => state.jobs.processingTimes);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
@@ -50,6 +52,7 @@ export default function FredyApp() {
|
||||
};
|
||||
|
||||
const isAdmin = () => currentUser != null && currentUser.isAdmin;
|
||||
const { Footer, Sider, Content } = Layout;
|
||||
|
||||
return loading ? null : needsLogin() ? (
|
||||
<Routes>
|
||||
@@ -57,71 +60,80 @@ export default function FredyApp() {
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
) : (
|
||||
<div className="app">
|
||||
<div className="app__container">
|
||||
<Logout />
|
||||
<Logo width={190} white />
|
||||
<Menu isAdmin={isAdmin()} />
|
||||
{versionUpdate?.newVersion && <VersionBanner />}
|
||||
{settings.demoMode && (
|
||||
<>
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
||||
<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 />} />
|
||||
<Route path="/listings" element={<Listings />} />
|
||||
<Layout className="app">
|
||||
<Layout className="app">
|
||||
<Sider>
|
||||
<Navigation isAdmin={isAdmin()} />
|
||||
</Sider>
|
||||
<Content>
|
||||
{versionUpdate?.newVersion && <VersionBanner />}
|
||||
{settings.demoMode && (
|
||||
<>
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
||||
/>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
||||
{processingTimes != null && <ProcessingTimes processingTimes={processingTimes} />}
|
||||
<Divider />
|
||||
<div className="app__content">
|
||||
<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 />} />
|
||||
<Route path="/listings" element={<Listings />} />
|
||||
|
||||
{/* Permission-aware routes */}
|
||||
<Route
|
||||
path="/users/new"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<UserMutator />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users/edit/:userId"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<UserMutator />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<Users />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/generalSettings"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<GeneralSettings />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
{/* Permission-aware routes */}
|
||||
<Route
|
||||
path="/users/new"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<UserMutator />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users/edit/:userId"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<UserMutator />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<Users />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/generalSettings"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser}>
|
||||
<GeneralSettings />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/" element={<Navigate to="/jobs" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
<Route path="/" element={<Navigate to="/jobs" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
<Footer>
|
||||
<FredyFooter />
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&__container {
|
||||
padding: 1rem 1rem;
|
||||
color: var(--semi-color-text-0);
|
||||
background-color: #232429;
|
||||
&__content {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
ui/src/components/footer/FredyFooter.jsx
Normal file
19
ui/src/components/footer/FredyFooter.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import './FredyFooter.less';
|
||||
import { useSelector } from '../../services/state/store.js';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
export default function FredyFooter() {
|
||||
const { Text } = Typography;
|
||||
const version = useSelector((state) => state.versionUpdate.versionUpdate);
|
||||
return (
|
||||
<div className="fredyFooter">
|
||||
<div className="fredyFooter__version">
|
||||
<Text type="tertiary">Fredy V{version?.localFredyVersion || 'N/A'}</Text>
|
||||
</div>
|
||||
<div className="fredyFooter__copyRight">
|
||||
<Text link={{ href: 'https://github.com/orangecoding', target: '_blank' }}>Made with ❤️</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
ui/src/components/footer/FredyFooter.less
Normal file
17
ui/src/components/footer/FredyFooter.less
Normal file
@@ -0,0 +1,17 @@
|
||||
.fredyFooter {
|
||||
background:rgb(53, 54, 60);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__version {
|
||||
padding-left: .5rem;
|
||||
font-size: small;
|
||||
|
||||
}
|
||||
&__copyRight {
|
||||
padding-right: 1rem;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,22 @@ import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { IconUser } from '@douyinfe/semi-icons';
|
||||
const Logout = function Logout() {
|
||||
|
||||
const Logout = function Logout({ text }) {
|
||||
return (
|
||||
<Button
|
||||
icon={<IconUser />}
|
||||
type="danger"
|
||||
theme="solid"
|
||||
onClick={async () => {
|
||||
await xhrPost('/api/login/logout');
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
icon={<IconUser />}
|
||||
type="danger"
|
||||
theme="solid"
|
||||
onClick={async () => {
|
||||
await xhrPost('/api/login/logout');
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
{text && 'Logout'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Tabs, TabPane } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { IconUser, IconTerminal, IconSetting, IconArchive } from '@douyinfe/semi-icons';
|
||||
import './Menu.less';
|
||||
|
||||
function parsePathName(name) {
|
||||
const split = name.split('/').filter((s) => s.length !== 0);
|
||||
return '/' + split[0];
|
||||
}
|
||||
|
||||
const TopMenu = function TopMenu({ isAdmin }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
return (
|
||||
<Tabs className="menu" type="line" activeKey={parsePathName(location.pathname)} onTabClick={(key) => navigate(key)}>
|
||||
<TabPane
|
||||
itemKey="/jobs"
|
||||
tab={
|
||||
<span>
|
||||
<IconTerminal />
|
||||
Jobs
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<TabPane
|
||||
itemKey="/listings"
|
||||
tab={
|
||||
<span>
|
||||
<IconArchive />
|
||||
Found listings
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
{isAdmin && (
|
||||
<TabPane
|
||||
itemKey="/users"
|
||||
tab={
|
||||
<span>
|
||||
<IconUser />
|
||||
User
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<TabPane
|
||||
itemKey="/generalSettings"
|
||||
tab={
|
||||
<span>
|
||||
<IconSetting />
|
||||
Settings
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopMenu;
|
||||
@@ -1,3 +0,0 @@
|
||||
.menu {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
9
ui/src/components/navigation/Navigate.less
Normal file
9
ui/src/components/navigation/Navigate.less
Normal file
@@ -0,0 +1,9 @@
|
||||
.navigate {
|
||||
&__logout_Button {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
}
|
||||
50
ui/src/components/navigation/Navigation.jsx
Normal file
50
ui/src/components/navigation/Navigation.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Nav } from '@douyinfe/semi-ui';
|
||||
import { IconUser, IconStar, IconSetting, IconTerminal } from '@douyinfe/semi-icons';
|
||||
import logoWhite from '../../assets/logo_white.png';
|
||||
import Logout from '../logout/Logout.jsx';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import './Navigate.less';
|
||||
import { useScreenWidth } from '../../hooks/screenWidth.js';
|
||||
|
||||
export default function Navigation({ isAdmin }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const width = useScreenWidth();
|
||||
const collapsed = width <= 850;
|
||||
|
||||
const items = [
|
||||
{ itemKey: '/jobs', text: 'Jobs', icon: <IconTerminal /> },
|
||||
{ itemKey: '/listings', text: 'Found Listings', icon: <IconStar /> },
|
||||
];
|
||||
|
||||
if (isAdmin) {
|
||||
items.push({ itemKey: '/users', text: 'User Management', icon: <IconUser /> });
|
||||
items.push({ itemKey: '/generalSettings', text: 'Settings', icon: <IconSetting /> });
|
||||
}
|
||||
|
||||
function parsePathName(name) {
|
||||
const split = name.split('/').filter((s) => s.length !== 0);
|
||||
return '/' + split[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<Nav
|
||||
style={{ height: '100%', width: collapsed ? '' : '13rem' }}
|
||||
items={items}
|
||||
isCollapsed={collapsed}
|
||||
selectedKeys={[parsePathName(location.pathname)]}
|
||||
onSelect={(key) => {
|
||||
navigate(key.itemKey);
|
||||
}}
|
||||
header={<img src={logoWhite} width="180" alt="Fredy Logo" />}
|
||||
footer={
|
||||
<div className="navigate__logout_Button">
|
||||
<Logout text={!collapsed} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export const SegmentPart = ({ name, Icon = null, children, helpText }) => {
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="segmentParts"
|
||||
title={
|
||||
<Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.segmentParts {
|
||||
border: 1px solid #323232 !important;
|
||||
border-radius: 5px !important;
|
||||
color: rgba(var(--semi-grey-8), 1);
|
||||
background: rgb(53, 54, 60);
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const empty = (
|
||||
<Empty
|
||||
image={<IllustrationNoResult />}
|
||||
darkModeImage={<IllustrationNoResultDark />}
|
||||
description={'No jobs available.'}
|
||||
description="No jobs available. Why don't you create one? ;)"
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Table, Popover, Input, Descriptions, Tag, Image } from '@douyinfe/semi-ui';
|
||||
import { Table, Popover, Input, Descriptions, Tag, Image, Empty } from '@douyinfe/semi-ui';
|
||||
import { useActions, useSelector } from '../../services/state/store.js';
|
||||
import { IconClose, IconSearch, IconTick } from '@douyinfe/semi-icons';
|
||||
import * as timeService from '../../services/time/timeService.js';
|
||||
@@ -8,6 +8,7 @@ import no_image from '../../assets/no_image.jpg';
|
||||
|
||||
import './ListingsTable.less';
|
||||
import { format } from '../../services/time/timeService.js';
|
||||
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -65,7 +66,7 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: 'Price',
|
||||
width: 100,
|
||||
width: 110,
|
||||
dataIndex: 'price',
|
||||
sorter: true,
|
||||
render: (text) => text + ' €',
|
||||
@@ -90,11 +91,19 @@ const columns = [
|
||||
},
|
||||
];
|
||||
|
||||
const empty = (
|
||||
<Empty
|
||||
image={<IllustrationNoResult />}
|
||||
darkModeImage={<IllustrationNoResultDark />}
|
||||
description="No listings available."
|
||||
/>
|
||||
);
|
||||
|
||||
export default function ListingsTable() {
|
||||
const tableData = useSelector((state) => state.listingsTable);
|
||||
const actions = useActions();
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 15;
|
||||
const pageSize = 10;
|
||||
const [sortData, setSortData] = useState({});
|
||||
const [filter, setFilter] = useState(null);
|
||||
|
||||
@@ -158,6 +167,7 @@ export default function ListingsTable() {
|
||||
/>
|
||||
<Table
|
||||
rowKey="id"
|
||||
empty={empty}
|
||||
hideExpandedColumn={false}
|
||||
sticky={{ top: 5 }}
|
||||
columns={columns}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Banner, Descriptions } from '@douyinfe/semi-ui';
|
||||
import { Collapse, Descriptions } from '@douyinfe/semi-ui';
|
||||
import { useSelector } from '../../services/state/store.js';
|
||||
import { MarkdownRender } from '@douyinfe/semi-ui';
|
||||
|
||||
@@ -8,12 +8,9 @@ import './VersionBanner.less';
|
||||
export default function VersionBanner() {
|
||||
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
|
||||
return (
|
||||
<Banner
|
||||
className="versionBanner"
|
||||
type="success"
|
||||
icon={null}
|
||||
description={
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
<Collapse>
|
||||
<Collapse.Panel header="A new version of Fredy is available" itemKey="1" className="versionBanner">
|
||||
<div className="versionBanner__content">
|
||||
<p>A new version of Fredy is available. Update now to take advantage of the latest features and bug fixes.</p>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey="Your Version">{versionUpdate.localFredyVersion}</Descriptions.Item>
|
||||
@@ -29,9 +26,9 @@ export default function VersionBanner() {
|
||||
<small>Release Notes</small>
|
||||
</b>
|
||||
</p>
|
||||
<MarkdownRender raw={versionUpdate.body} style={{ height: '200px' }} />
|
||||
<MarkdownRender raw={versionUpdate.body} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.versionBanner {
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(var(--semi-teal-1), 1);
|
||||
|
||||
&__content {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
23
ui/src/hooks/screenWidth.js
Normal file
23
ui/src/hooks/screenWidth.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useScreenWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => setWidth(window.innerWidth), 100);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { useActions, useSelector } from '../../services/state/store';
|
||||
|
||||
import { Divider, TimePicker, Button, Checkbox, Input } from '@douyinfe/semi-ui';
|
||||
import { InputNumber } from '@douyinfe/semi-ui';
|
||||
import Headline from '../../components/headline/Headline';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||
import { Banner, Toast } from '@douyinfe/semi-ui';
|
||||
@@ -125,7 +124,6 @@ const GeneralSettings = function GeneralSettings() {
|
||||
<div>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<Headline text="General Settings" />
|
||||
<div>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
|
||||
@@ -4,14 +4,12 @@ import JobTable from '../../components/table/JobTable';
|
||||
import { useSelector, useActions } from '../../services/state/store';
|
||||
import { xhrDelete, xhrPut } from '../../services/xhr';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ProcessingTimes from './ProcessingTimes';
|
||||
import { Button, Toast } from '@douyinfe/semi-ui';
|
||||
import { IconPlusCircle } from '@douyinfe/semi-icons';
|
||||
import './Jobs.less';
|
||||
|
||||
export default function Jobs() {
|
||||
const jobs = useSelector((state) => state.jobs.jobs);
|
||||
const processingTimes = useSelector((state) => state.jobs.processingTimes);
|
||||
const navigate = useNavigate();
|
||||
const actions = useActions();
|
||||
|
||||
@@ -38,7 +36,6 @@ export default function Jobs() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{processingTimes != null && <ProcessingTimes processingTimes={processingTimes} />}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlusCircle />}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.jobs {
|
||||
&__newButton {
|
||||
margin-top: 1rem !important;
|
||||
float: right;
|
||||
float: left;
|
||||
margin-bottom: 1rem !important;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
import React from 'react';
|
||||
import { format } from '../../services/time/timeService';
|
||||
import { Button, Descriptions, Toast } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, Col, Row, Toast } from '@douyinfe/semi-ui';
|
||||
import { IconPlayCircle } from '@douyinfe/semi-icons';
|
||||
import { xhrPost } from '../../services/xhr.js';
|
||||
|
||||
import './ProsessingTimes.less';
|
||||
|
||||
function InfoCard({ title, value }) {
|
||||
return (
|
||||
<Card style={{ maxWidth: '13rem', margin: '1rem', background: 'rgb(53, 54, 60)' }} title={title}>
|
||||
{value}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessingTimes({ processingTimes = {} }) {
|
||||
if (Object.keys(processingTimes).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Descriptions
|
||||
row
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#35363c',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Next run">
|
||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Find Listings now">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<IconPlayCircle />}
|
||||
aria-label="Start now"
|
||||
onClick={async () => {
|
||||
await xhrPost('/api/jobs/startAll', null);
|
||||
Toast.success('Successfully triggered Fredy search.');
|
||||
}}
|
||||
>
|
||||
Search now
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<InfoCard title="Processing Interval" value={`${processingTimes.interval} min`} />
|
||||
</Col>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Col span={6}>
|
||||
<InfoCard title="Last run" value={format(processingTimes.lastRun)} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<InfoCard title="Next run" value={format(processingTimes.lastRun + processingTimes.interval * 60000)} />
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
<Col span={6}>
|
||||
<InfoCard
|
||||
title="Find Listings Now"
|
||||
value={
|
||||
<Button
|
||||
size="small"
|
||||
icon={<IconPlayCircle />}
|
||||
aria-label="Start now"
|
||||
onClick={async () => {
|
||||
await xhrPost('/api/jobs/startAll', null);
|
||||
Toast.success('Successfully triggered Fredy search.');
|
||||
}}
|
||||
>
|
||||
Search now
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
5
ui/src/views/jobs/ProsessingTimes.less
Normal file
5
ui/src/views/jobs/ProsessingTimes.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.processingTimes {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ const Users = function Users() {
|
||||
icon={<IconPlus />}
|
||||
onClick={() => navigate('/users/new')}
|
||||
>
|
||||
Create new User
|
||||
New User
|
||||
</Button>
|
||||
|
||||
<UserTable
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.users {
|
||||
&__newButton {
|
||||
margin-top: 1rem !important;
|
||||
float: right;
|
||||
float: left;
|
||||
margin-bottom: 1rem !important;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
52
yarn.lock
52
yarn.lock
@@ -1428,10 +1428,10 @@
|
||||
"@resvg/resvg-js-win32-ia32-msvc" "2.4.1"
|
||||
"@resvg/resvg-js-win32-x64-msvc" "2.4.1"
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-beta.35":
|
||||
version "1.0.0-beta.35"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz#1a477e7742b154b67519d40e4fc17485de338e7a"
|
||||
integrity sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==
|
||||
"@rolldown/pluginutils@1.0.0-beta.38":
|
||||
version "1.0.0-beta.38"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz#95253608c4629eb2a5f3d656009ac9ba031eb292"
|
||||
integrity sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.49.0":
|
||||
version "4.49.0"
|
||||
@@ -1895,15 +1895,15 @@
|
||||
"@turf/invariant" "^6.5.0"
|
||||
eventemitter3 "^4.0.7"
|
||||
|
||||
"@vitejs/plugin-react@5.0.3":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz#182ea45406d89e55b4e35c92a4a8c2c8388726c8"
|
||||
integrity sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg==
|
||||
"@vitejs/plugin-react@5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz#d642058e89c5b712655c8cbd13482f5813519602"
|
||||
integrity sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.28.4"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.27.1"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.27.1"
|
||||
"@rolldown/pluginutils" "1.0.0-beta.35"
|
||||
"@rolldown/pluginutils" "1.0.0-beta.38"
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.17.0"
|
||||
|
||||
@@ -2362,10 +2362,10 @@ ccount@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
chai@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-6.0.1.tgz#88c2b4682fb56050647e222d2cf9d6772f2607b3"
|
||||
integrity sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==
|
||||
chai@6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.0.tgz#181bca6a219cddb99c3eeefb82483800ffa550ce"
|
||||
integrity sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
version "4.1.2"
|
||||
@@ -4559,10 +4559,10 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
lint-staged@16.2.1:
|
||||
version "16.2.1"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.2.1.tgz#bb82da8ce10059296b220f321980f0ee1ce40c28"
|
||||
integrity sha512-KMeYmH9wKvHsXdUp+z6w7HN3fHKHXwT1pSTQTYxB9kI6ekK1rlL3kLZEoXZCppRPXFK9PFW/wfQctV7XUqMrPQ==
|
||||
lint-staged@16.2.3:
|
||||
version "16.2.3"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.2.3.tgz#790866221d75602510507b5be40b2c7963715960"
|
||||
integrity sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==
|
||||
dependencies:
|
||||
commander "^14.0.1"
|
||||
listr2 "^9.0.4"
|
||||
@@ -6121,17 +6121,17 @@ react-resizable@^3.0.5:
|
||||
prop-types "15.x"
|
||||
react-draggable "^4.0.3"
|
||||
|
||||
react-router-dom@7.9.2:
|
||||
version "7.9.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.2.tgz#2bb35d226ca23329f4e39c8f86d1db26ee4fdf26"
|
||||
integrity sha512-pagqpVJnjZOfb+vIM23eTp7Sp/AAJjOgaowhP1f1TWOdk5/W8Uk8d/M/0wfleqx7SgjitjNPPsKeCZE1hTSp3w==
|
||||
react-router-dom@7.9.3:
|
||||
version "7.9.3"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.3.tgz#67ab1655f67b9b6108fe20ed3d4881b53dccf87a"
|
||||
integrity sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==
|
||||
dependencies:
|
||||
react-router "7.9.2"
|
||||
react-router "7.9.3"
|
||||
|
||||
react-router@7.9.2:
|
||||
version "7.9.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.2.tgz#f424a14f87e4d7b5b268ce3647876e9504e4fca6"
|
||||
integrity sha512-i2TPp4dgaqrOqiRGLZmqh2WXmbdFknUyiCRmSKs0hf6fWXkTKg5h56b+9F22NbGRAMxjYfqQnpi63egzD2SuZA==
|
||||
react-router@7.9.3:
|
||||
version "7.9.3"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.3.tgz#f2d5ff6181851de3df3acb4e7364fce0dee5fba2"
|
||||
integrity sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==
|
||||
dependencies:
|
||||
cookie "^1.0.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
|
||||
Reference in New Issue
Block a user