feat: Fredy UI redesign

* New design :)
This commit is contained in:
Christian Kellner
2026-04-22 21:11:18 +02:00
committed by GitHub
parent c78472bd19
commit f30ec4645c
43 changed files with 4004 additions and 794 deletions

View File

@@ -4,7 +4,7 @@
*/
import React from 'react';
import { Button, Col, Row, Toast, Typography } from '@douyinfe/semi-ui-19';
import { Button, Col, Row, Toast } from '@douyinfe/semi-ui-19';
import {
IconTerminal,
IconStar,
@@ -20,6 +20,7 @@ import {
import { useSelector, useActions } from '../../services/state/store';
import KpiCard from '../../components/cards/KpiCard.jsx';
import PieChartCard from '../../components/cards/PieChartCard.jsx';
import Headline from '../../components/headline/Headline.jsx';
import './Dashboard.less';
import { xhrPost } from '../../services/xhr.js';
@@ -34,11 +35,12 @@ export default function Dashboard() {
const kpis = dashboard?.kpis || { totalJobs: 0, totalListings: 0, providersUsed: 0 };
const pieData = dashboard?.pie || [];
const { Text } = Typography;
return (
<div className="dashboard">
<Text className="dashboard__section-label">General</Text>
<Headline text="Dashboard" />
<div className="dashboard__section-label">General</div>
<Row gutter={[16, 16]} className="dashboard__row">
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
<KpiCard
@@ -51,7 +53,6 @@ export default function Dashboard() {
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
<KpiCard
title="Last Search"
valueFontSize="14px"
value={
dashboard?.general?.lastRun == null || dashboard?.general?.lastRun === 0
? '---'
@@ -69,7 +70,6 @@ export default function Dashboard() {
? '---'
: format(dashboard?.general?.nextRun)
}
valueFontSize="14px"
icon={<IconDoubleChevronRight />}
description="Next execution timestamp"
/>
@@ -96,7 +96,7 @@ export default function Dashboard() {
</Col>
</Row>
<Text className="dashboard__section-label">Overview</Text>
<div className="dashboard__section-label">Overview</div>
<Row gutter={[16, 16]} className="dashboard__row">
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
<KpiCard
@@ -132,10 +132,9 @@ export default function Dashboard() {
value={`${
!kpis.medianPriceOfListings
? '---'
: new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(kpis.medianPriceOfListings)
: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(
kpis.medianPriceOfListings,
)
}`}
icon={<IconNoteMoney />}
description="Median Price of listings"
@@ -143,7 +142,7 @@ export default function Dashboard() {
</Col>
</Row>
<Text className="dashboard__section-label">Provider Insights</Text>
<div className="dashboard__section-label">Provider Insights</div>
<div className="dashboard__pie-wrapper">
<PieChartCard data={pieData} />
</div>

View File

@@ -1,3 +1,5 @@
@import '../../tokens.less';
.dashboard {
display: flex;
flex-direction: column;
@@ -5,13 +7,13 @@
&__section-label {
display: block;
font-size: 11px !important;
font-weight: 600 !important;
font-size: @text-xs;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #5a6478 !important;
color: @color-faint;
margin-bottom: 10px;
margin-top: 4px;
margin-top: 1.5rem;
}
&__row {
@@ -22,9 +24,8 @@
&__pie-wrapper {
background: #23242a;
border: 1px solid #37404e;
border-radius: 10px;
padding: 24px;
border-radius: @radius-card;
padding: 28px;
max-height: 320px;
flex: 1;
display: flex;

View File

@@ -30,6 +30,7 @@ import {
} from '../../services/backupRestoreClient';
import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder } from '@douyinfe/semi-icons';
import { debounce } from '../../utils';
import Headline from '../../components/headline/Headline.jsx';
import './GeneralSettings.less';
function formatFromTimestamp(ts) {
@@ -244,6 +245,7 @@ const GeneralSettings = function GeneralSettings() {
return (
<div className="generalSettings">
<Headline text="Settings" />
{!loading && (
<>
<Tabs type="line">

View File

@@ -1,17 +1,73 @@
@import '../../tokens.less';
.generalSettings {
display: flex;
flex-direction: column;
flex: 1;
&__tab-content {
padding: 20px 0;
max-width: 860px;
padding: @space-4 0;
}
&__timePickerContainer {
display: flex;
align-items: baseline;
gap: 1rem;
gap: @space-3;
flex-wrap: wrap;
align-items: center;
}
&__save-row {
margin-top: 1.5rem;
display: flex;
justify-content: flex-end;
margin-top: @space-2;
}
}
// InputNumber fix
.semi-input-number {
background: @color-elevated !important;
border: 1px solid @color-border-bright !important;
border-radius: @radius-input !important;
color: @color-text !important;
}
.semi-input-number-button-up,
.semi-input-number-button-down {
background: rgba(255,255,255,0.06) !important;
border-color: @color-border-bright !important;
color: @color-muted !important;
&:hover {
background: rgba(255,255,255,0.12) !important;
color: @color-text !important;
}
}
// TimePicker fix — scoped so it doesn't pollute modal headers
.semi-timepicker .semi-input-wrapper,
.semi-timepicker .semi-input-inset-label-wrapper {
background: @color-elevated !important;
border: 1px solid @color-border-bright !important;
border-radius: @radius-input !important;
color: @color-text !important;
}
// Tabs styling
.semi-tabs-bar-line .semi-tabs-tab {
color: @color-faint;
font-size: @text-base;
transition: color @transition-fast;
&:hover {
color: @color-muted;
}
&.semi-tabs-tab-active {
color: @color-text !important;
}
}
.semi-tabs-bar-line .semi-tabs-ink-bar {
background-color: @color-accent !important;
height: 2px;
}

View File

@@ -3,12 +3,25 @@
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { useNavigate } from 'react-router-dom';
import { Button } from '@douyinfe/semi-ui-19';
import { IconPlusCircle } from '@douyinfe/semi-icons';
import JobGrid from '../../components/grid/jobs/JobGrid.jsx';
import Headline from '../../components/headline/Headline.jsx';
import './Jobs.less';
export default function Jobs() {
const navigate = useNavigate();
return (
<div className="jobs">
<Headline
text="Jobs"
actions={
<Button type="primary" theme="solid" icon={<IconPlusCircle />} onClick={() => navigate('/jobs/new')}>
New Job
</Button>
}
/>
<JobGrid />
</div>
);

View File

@@ -1,8 +1,7 @@
@import '../../tokens.less';
.jobs {
&__newButton {
margin-top: 1rem !important;
float: left;
margin-bottom: 1rem !important;
margin-left: 1rem;
}
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -18,6 +18,7 @@ import { Divider, Input, Switch, Button, TagInput, Toast, Select } from '@douyin
import './JobMutation.less';
import { SegmentPart } from '../../../components/segment/SegmentPart';
import {
IconArrowLeft,
IconBell,
IconBriefcase,
IconPaperclip,
@@ -144,7 +145,19 @@ export default function JobMutator() {
/>
)}
<Headline text={jobToBeEdit ? 'Edit Job' : 'Create new Job'} />
<Headline
text={jobToBeEdit ? 'Edit Job' : 'Create new Job'}
actions={
<Button
icon={<IconArrowLeft />}
onClick={() => navigate('/jobs')}
theme="borderless"
style={{ color: '#909090' }}
>
Back
</Button>
}
/>
<form>
<SegmentPart name="Name" Icon={IconPaperclip}>
<Input

View File

@@ -1,25 +1,36 @@
@import '../../../tokens.less';
.jobMutation {
&__newButton {
float: right;
margin-bottom: 1rem;
margin-bottom: @space-4;
}
&__specFilter {
display: flex;
gap: 1.5rem;
gap: @space-4;
flex-wrap: wrap;
}
&__specFilterItem {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: @space-2;
flex: 1;
min-width: 150px;
}
&__specFilterLabel {
font-weight: 500;
font-size: @text-sm;
color: @color-muted;
}
&__actions {
display: flex;
gap: @space-3;
margin-top: @space-4;
justify-content: flex-end;
}
}

View File

@@ -41,6 +41,7 @@ import * as timeService from '../../services/time/timeService.js';
import { distanceMeters, getBoundsFromCoords } from './mapUtils.js';
import { xhrPost } from '../../services/xhr.js';
import Headline from '../../components/headline/Headline.jsx';
import './ListingDetail.less';
const { Title, Text } = Typography;
@@ -278,7 +279,7 @@ export default function ListingDetail() {
},
{
key: 'Provider',
value: listing.provider.charAt(0).toUpperCase() + listing.provider.slice(1),
value: listing.provider ? listing.provider.charAt(0).toUpperCase() + listing.provider.slice(1) : 'Unknown',
Icon: <IconBriefcase />,
},
{
@@ -290,40 +291,45 @@ export default function ListingDetail() {
return (
<div className="listing-detail">
<div className="listing-detail__back">
<Button icon={<IconArrowLeft />} onClick={() => navigate(-1)} theme="borderless">
Back
</Button>
</div>
<Headline
text={listing?.title || 'Listing Detail'}
actions={
<Button icon={<IconArrowLeft />} onClick={() => navigate(-1)} theme="borderless" style={{ color: '#909090' }}>
Back
</Button>
}
/>
<Card className="listing-detail__card">
<div className="listing-detail__header">
<Space vertical align="start" spacing="tight">
<Title heading={2} className="listing-detail__title">
{listing.title}
</Title>
<Space align="center">
<IconMapPin style={{ fontSize: '18px', color: 'var(--semi-color-primary)' }} />
<Text type="secondary">{listing.address || 'No address provided'}</Text>
</Space>
<Space align="center">
<IconMapPin style={{ fontSize: '18px', color: 'var(--semi-color-primary)' }} />
{listing.address ? (
<a
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(listing.address)}`}
target="_blank"
rel="noopener noreferrer"
className="listing-detail__address-link"
>
{listing.address}
</a>
) : (
<Text type="secondary">No address provided</Text>
)}
</Space>
<Space wrap className="listing-detail__header-actions">
<Button
icon={
listing.isWatched === 1 ? (
<IconStar style={{ color: 'var(--semi-color-warning)' }} />
) : (
<IconStarStroked />
)
}
icon={listing.isWatched === 1 ? <IconStar /> : <IconStarStroked />}
onClick={handleWatch}
theme="light"
theme="borderless"
className={`listing-detail__watch-btn${listing.isWatched === 1 ? ' listing-detail__watch-btn--active' : ''}`}
>
{listing.isWatched === 1 ? 'Watched' : 'Watch'}
</Button>
<Text link={{ href: listing.link, target: '_blank' }} icon={<IconLink />} underline>
<a href={listing.link} target="_blank" rel="noopener noreferrer" className="listing-detail__open-btn">
<IconLink style={{ marginRight: 6 }} />
Open listing
</Text>
</a>
</Space>
</div>

View File

@@ -1,10 +1,8 @@
@import '../../tokens.less';
.listing-detail {
padding-bottom: 2rem;
&__back {
margin-bottom: 1.5rem;
}
&__card {
background-color: rgba(36, 36, 36, 0.9);
backdrop-filter: blur(8px);
@@ -45,14 +43,6 @@
}
}
&__title {
margin: 0 !important;
word-break: break-word;
@media (max-width: 768px) {
font-size: 1.5rem;
}
}
&__image-container {
width: 100%;
height: 400px;
@@ -69,7 +59,61 @@
img {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
}
.semi-image,
.semi-image-img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block !important;
}
}
&__address-link {
color: @color-muted;
text-decoration: none;
font-size: @text-base;
transition: color @transition-fast;
&:hover {
color: @color-text;
text-decoration: underline;
}
}
&__watch-btn {
color: @color-muted !important;
border: 1px solid @color-border-bright !important;
border-radius: @radius-btn !important;
&:hover {
color: @color-text !important;
background: rgba(255,255,255,0.06) !important;
}
&--active {
color: @color-accent !important;
border-color: rgba(224,74,56,0.4) !important;
background: rgba(224,74,56,0.08) !important;
}
}
&__open-btn {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
border: 1px solid @color-border-bright;
border-radius: @radius-btn;
color: @color-muted;
font-size: @text-base;
font-family: @font-ui;
font-weight: 500;
text-decoration: none;
transition: color @transition-fast, border-color @transition-fast, background @transition-fast;
&:hover {
color: @color-text;
border-color: rgba(255,255,255,0.25);
background: rgba(255,255,255,0.06);
}
}

View File

@@ -4,7 +4,13 @@
*/
import ListingsGrid from '../../components/grid/listings/ListingsGrid.jsx';
import Headline from '../../components/headline/Headline.jsx';
export default function Listings() {
return <ListingsGrid />;
return (
<>
<Headline text="Listings" />
<ListingsGrid />
</>
);
}

View File

@@ -21,6 +21,7 @@ import { xhrDelete } from '../../services/xhr.js';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
import Map from '../../components/map/Map.jsx';
import Headline from '../../components/headline/Headline.jsx';
const RangeSlider = _RangeSlider?.default ?? _RangeSlider;
@@ -354,127 +355,130 @@ export default function MapView() {
}, [listings, priceRange, homeAddress, distanceFilter]);
return (
<div className="map-view-container">
{!homeAddress && (
<>
<Headline text="Map View" />
<div className="map-view-container">
{!homeAddress && (
<Banner
fullMode={true}
type="warning"
bordered
closeIcon={null}
style={{ marginBottom: '8px' }}
description={
<span>
No home address set. Configure it in <Link to="/userSettings">user settings</Link> to use the distance
filter.
</span>
}
/>
)}
<Banner
fullMode={true}
type="warning"
type="info"
bordered
closeIcon={null}
style={{ marginBottom: '8px' }}
description={
<span>
No home address set. Configure it in <Link to="/userSettings">user settings</Link> to use the distance
filter.
</span>
}
/>
)}
<Banner
fullMode={true}
type="info"
bordered
closeIcon={null}
style={{ marginBottom: '8px' }}
description="Only listings with valid addresses are shown on this map."
/>
<div className="map-view-container__map-wrapper">
<Map
mapContainerRef={mapContainer}
style={style}
show3dBuildings={show3dBuildings}
onMapReady={handleMapReady}
description="Only listings with valid addresses are shown on this map."
/>
{/* Floating filter panel */}
<div className="map-view-container__floating-panel">
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Job
</Text>
<Select
placeholder="All jobs"
showClear
size="small"
onChange={(val) => setJobId(val)}
value={jobId}
style={{ width: 160 }}
>
{jobs?.map((j) => (
<Select.Option key={j.id} value={j.id}>
{j.name}
</Select.Option>
))}
</Select>
</div>
<div className="map-view-container__map-wrapper">
<Map
mapContainerRef={mapContainer}
style={style}
show3dBuildings={show3dBuildings}
onMapReady={handleMapReady}
/>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Distance
</Text>
<Select
placeholder="None"
size="small"
onChange={(val) => setDistanceFilter(val)}
value={distanceFilter}
style={{ width: 100 }}
>
<Select.Option value={0}>None</Select.Option>
<Select.Option value={5}>5 km</Select.Option>
<Select.Option value={10}>10 km</Select.Option>
<Select.Option value={15}>15 km</Select.Option>
<Select.Option value={20}>20 km</Select.Option>
<Select.Option value={25}>25 km</Select.Option>
</Select>
</div>
{/* Floating filter panel */}
<div className="map-view-container__floating-panel">
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Job
</Text>
<Select
placeholder="All jobs"
showClear
size="small"
onChange={(val) => setJobId(val)}
value={jobId}
style={{ width: 160 }}
>
{jobs?.map((j) => (
<Select.Option key={j.id} value={j.id}>
{j.name}
</Select.Option>
))}
</Select>
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Price ()
</Text>
<div className="map-view-container__price-slider">
<div className="map__rangesliderLabels">
<span>{priceRange[0]}</span>
<span>{priceRange[1]}</span>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Distance
</Text>
<Select
placeholder="None"
size="small"
onChange={(val) => setDistanceFilter(val)}
value={distanceFilter}
style={{ width: 100 }}
>
<Select.Option value={0}>None</Select.Option>
<Select.Option value={5}>5 km</Select.Option>
<Select.Option value={10}>10 km</Select.Option>
<Select.Option value={15}>15 km</Select.Option>
<Select.Option value={20}>20 km</Select.Option>
<Select.Option value={25}>25 km</Select.Option>
</Select>
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Price ()
</Text>
<div className="map-view-container__price-slider">
<div className="map__rangesliderLabels">
<span>{priceRange[0]}</span>
<span>{priceRange[1]}</span>
</div>
<RangeSlider min={0} max={getMaxPrice()} step={100} value={priceRange} onInput={handlePriceRange} />
</div>
<RangeSlider min={0} max={getMaxPrice()} step={100} value={priceRange} onInput={handlePriceRange} />
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Style
</Text>
<Select size="small" value={style} onChange={(val) => handleMapStyle(val)} style={{ width: 110 }}>
<Select.Option value="STANDARD">Standard</Select.Option>
<Select.Option value="SATELLITE">Satellite</Select.Option>
</Select>
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
3D Buildings
</Text>
<Switch
size="small"
checked={show3dBuildings}
onChange={(v) => setShow3dBuildings(v)}
disabled={style === 'SATELLITE'}
/>
</div>
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
Style
</Text>
<Select size="small" value={style} onChange={(val) => handleMapStyle(val)} style={{ width: 110 }}>
<Select.Option value="STANDARD">Standard</Select.Option>
<Select.Option value="SATELLITE">Satellite</Select.Option>
</Select>
</div>
<div className="map-view-container__panel-row">
<Text size="small" strong style={{ color: '#8892a4' }}>
3D Buildings
</Text>
<Switch
size="small"
checked={show3dBuildings}
onChange={(v) => setShow3dBuildings(v)}
disabled={style === 'SATELLITE'}
/>
</div>
</div>
</div>
<ListingDeletionModal
visible={deleteModalVisible}
onConfirm={confirmListingDeletion}
onCancel={() => {
setDeleteModalVisible(false);
setListingToDelete(null);
}}
/>
</div>
<ListingDeletionModal
visible={deleteModalVisible}
onConfirm={confirmListingDeletion}
onCancel={() => {
setDeleteModalVisible(false);
setListingToDelete(null);
}}
/>
</div>
</>
);
}

View File

@@ -3,6 +3,8 @@
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
@import '../../tokens.less';
.map-view-container {
display: flex;
flex-direction: column;
@@ -21,11 +23,11 @@
top: 12px;
right: 12px;
z-index: 10;
background: rgba(13, 15, 20, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid #262a3a;
border-radius: 10px;
background: rgba(22, 25, 38, 0.95);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid @color-border;
border-radius: @radius-card;
padding: 14px 16px;
min-width: 220px;
display: flex;
@@ -183,13 +185,19 @@
position: absolute;
z-index: 3;
top: 50%;
width: 14px;
height: 14px;
width: 12px;
height: 12px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: #0ab5b3;
background: @color-accent;
}
.range-slider .range-slider__range {
background: #0ab5b3;
background: @color-accent;
}
.range-slider {
background: rgba(255,255,255,0.12);
border-radius: 4px;
height: 4px !important;
}

View File

@@ -6,9 +6,8 @@
import { useState } from 'react';
import { IconHorn } from '@douyinfe/semi-icons';
import { SegmentPart } from '../../../components/segment/SegmentPart.jsx';
import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui-19';
import { Banner, Button, Checkbox, Space, Typography } from '@douyinfe/semi-ui-19';
import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx';
import Headline from '../../../components/headline/Headline.jsx';
export default function WatchlistManagement() {
const [notificationChooserVisible, setNotificationChooserVisible] = useState(false);
@@ -31,7 +30,9 @@ export default function WatchlistManagement() {
description="Youll receive notifications only for listings that are on your watch list. To add listings to it, open the 'Listings' section and tag the ones you want to follow."
/>
<Space />
<Headline size={5} text="Notify me when:" style={{ marginTop: '1rem' }} />
<Typography.Title heading={5} style={{ marginTop: '1rem' }}>
Notify me when:
</Typography.Title>
<Checkbox checked={activityChanges} onChange={(e) => setActivityChanges(e.target.checked)}>
Listing state changes (e.g. listing becomes inactive)
@@ -41,7 +42,9 @@ export default function WatchlistManagement() {
</Checkbox>
<Space />
<Headline size={5} text="Notify me with:" style={{ marginTop: '1rem' }} />
<Typography.Title heading={5} style={{ marginTop: '1rem' }}>
Notify me with:
</Typography.Title>
<Button onClick={() => setNotificationChooserVisible(true)}>Select notification method</Button>
<NotificationAdapterMutator

View File

@@ -30,7 +30,7 @@
border-radius: 24px;
padding: 3rem;
width: 90%;
max-width: 420px;
max-width: 500px;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -4,16 +4,14 @@
*/
import React from 'react';
import { Toast } from '@douyinfe/semi-ui-19';
import { Toast, Button } from '@douyinfe/semi-ui-19';
import { IconPlus } from '@douyinfe/semi-icons';
import UserTable from '../../components/table/UserTable';
import { useActions, useSelector } from '../../services/state/store';
import { IconPlus } from '@douyinfe/semi-icons';
import { Button } from '@douyinfe/semi-ui-19';
import UserRemovalModal from './UserRemovalModal';
import { xhrDelete } from '../../services/xhr';
import { useNavigate } from 'react-router-dom';
import Headline from '../../components/headline/Headline.jsx';
import './Users.less';
const Users = function Users() {
@@ -28,14 +26,13 @@ const Users = function Users() {
await actions.user.getUsers();
setLoading(false);
}
init();
}, []);
const onUserRemoval = async () => {
try {
await xhrDelete('/api/admin/users', { userId: userIdToBeRemoved });
Toast.success('User successfully remove');
Toast.success('User successfully removed');
setUserIdToBeRemoved(null);
await actions.jobsData.getJobs();
await actions.user.getUsers();
@@ -46,30 +43,22 @@ const Users = function Users() {
};
return (
<div>
<div className="users">
<Headline
text="Users"
actions={
<Button type="primary" theme="solid" icon={<IconPlus />} onClick={() => navigate('/users/new')}>
New User
</Button>
}
/>
{!loading && (
<React.Fragment>
{userIdToBeRemoved && <UserRemovalModal onCancel={() => setUserIdToBeRemoved(null)} onOk={onUserRemoval} />}
<Button
type="primary"
className="users__newButton"
icon={<IconPlus />}
onClick={() => navigate('/users/new')}
>
New User
</Button>
<UserTable
user={users}
onUserEdit={(userId) => {
navigate(`/users/edit/${userId}`);
}}
onUserRemoval={(userId) => {
setUserIdToBeRemoved(userId);
//throw warning message that all jobs will be removed associated to this user
//check if at least 1 admin is available
}}
onUserEdit={(userId) => navigate(`/users/edit/${userId}`)}
onUserRemoval={(userId) => setUserIdToBeRemoved(userId)}
/>
</React.Fragment>
)}

View File

@@ -1,8 +1,7 @@
@import '../../tokens.less';
.users {
&__newButton {
margin-top: 1rem !important;
float: left;
margin-bottom: 1rem !important;
margin-left: 1rem;
}
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -11,7 +11,8 @@ import { useActions } from '../../../services/state/store';
import { Divider, Input, Switch, Button, Toast } from '@douyinfe/semi-ui-19';
import './UserMutator.less';
import { SegmentPart } from '../../../components/segment/SegmentPart';
import { IconPlusCircle } from '@douyinfe/semi-icons';
import { IconPlusCircle, IconArrowLeft } from '@douyinfe/semi-icons';
import Headline from '../../../components/headline/Headline.jsx';
const UserMutator = function UserMutator() {
const params = useParams();
@@ -63,53 +64,70 @@ const UserMutator = function UserMutator() {
};
return (
<form className="userMutator">
<SegmentPart name="Username" helpText="The username used to login to Fredy">
<Input
type="text"
label="Username"
maxLength={30}
placeholder="Username"
autoFocus
width={6}
value={username}
onChange={(val) => setUsername(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Password" helpText="The password used to login to Fredy">
<Input
mode="password"
label="Password"
placeholder="Password"
width={6}
value={password}
onChange={(val) => setPassword(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Retype password" helpText="Retype the password to make sure they match">
<Input
mode="password"
label="Retype password"
placeholder="Retype password"
width={6}
value={password2}
onChange={(val) => setPassword2(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Is user an admin?" helpText="Check this if the user is an administrator">
<Switch checked={isAdmin} onChange={(checked) => setIsAdmin(checked)} />
</SegmentPart>
<Divider margin="1rem" />
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => navigate('/users')}>
Cancel
</Button>
<Button type="primary" icon={<IconPlusCircle />} onClick={saveUser}>
Save
</Button>
</form>
<>
<Headline
text={params.userId ? 'Edit User' : 'New User'}
actions={
<Button
icon={<IconArrowLeft />}
onClick={() => navigate('/users')}
theme="borderless"
style={{ color: '#909090' }}
>
Back
</Button>
}
/>
<form className="userMutator">
<SegmentPart name="Username" helpText="The username used to login to Fredy">
<Input
type="text"
label="Username"
maxLength={30}
placeholder="Username"
autoFocus
width={6}
value={username}
onChange={(val) => setUsername(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Password" helpText="The password used to login to Fredy">
<Input
mode="password"
label="Password"
placeholder="Password"
width={6}
value={password}
onChange={(val) => setPassword(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Retype password" helpText="Retype the password to make sure they match">
<Input
mode="password"
label="Retype password"
placeholder="Retype password"
width={6}
value={password2}
onChange={(val) => setPassword2(val)}
/>
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart name="Is user an admin?" helpText="Check this if the user is an administrator">
<Switch checked={isAdmin} onChange={(checked) => setIsAdmin(checked)} />
</SegmentPart>
<Divider margin="1rem" />
<div className="userMutator__actions">
<Button size="small" theme="borderless" style={{ color: '#909090' }} onClick={() => navigate('/users')}>
Cancel
</Button>
<Button size="small" type="primary" theme="solid" icon={<IconPlusCircle />} onClick={saveUser}>
Save
</Button>
</div>
</form>
</>
);
};

View File

@@ -1,3 +1,13 @@
@import '../../../tokens.less';
.userMutator {
margin-top: 2rem;
display: flex;
flex-direction: column;
&__actions {
display: flex;
gap: @space-2;
margin-top: @space-2;
justify-content: flex-start;
}
}