adding features

This commit is contained in:
orangecoding
2025-12-09 13:40:13 +01:00
parent 9a6ae5c30a
commit 89c93efa17
9 changed files with 78 additions and 17 deletions

View File

@@ -16,6 +16,7 @@ import { demoRouter } from './routes/demoRouter.js';
import logger from '../services/logger.js'; import logger from '../services/logger.js';
import { listingsRouter } from './routes/listingsRouter.js'; import { listingsRouter } from './routes/listingsRouter.js';
import { getSettings } from '../services/storage/settingsStorage.js'; import { getSettings } from '../services/storage/settingsStorage.js';
import { featureRouter } from './routes/featureRouter.js';
const service = restana(); const service = restana();
const staticService = files(path.join(getDirName(), '../ui/public')); const staticService = files(path.join(getDirName(), '../ui/public'));
const PORT = (await getSettings()).port || 9998; const PORT = (await getSettings()).port || 9998;
@@ -39,6 +40,7 @@ service.use('/api/version', versionRouter);
service.use('/api/jobs', jobRouter); service.use('/api/jobs', jobRouter);
service.use('/api/login', loginRouter); service.use('/api/login', loginRouter);
service.use('/api/listings', listingsRouter); service.use('/api/listings', listingsRouter);
service.use('/api/features', featureRouter);
//this route is unsecured intentionally as it is being queried from the login page //this route is unsecured intentionally as it is being queried from the login page
service.use('/api/demo', demoRouter); service.use('/api/demo', demoRouter);

View File

@@ -0,0 +1,12 @@
import restana from 'restana';
import getFeatures from '../../features.js';
const service = restana();
const featureRouter = service.newRouter();
featureRouter.get('/', async (req, res) => {
const features = getFeatures();
res.body = Object.assign({}, { features });
res.send();
});
export { featureRouter };

9
lib/features.js Normal file
View File

@@ -0,0 +1,9 @@
const FEATURES = {
WATCHLIST_MANAGEMENT: false,
};
export default function getFeatures() {
return {
...FEATURES,
};
}

View File

@@ -21,7 +21,7 @@ import Navigation from './components/navigation/Navigation.jsx';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import FredyFooter from './components/footer/FredyFooter.jsx'; import FredyFooter from './components/footer/FredyFooter.jsx';
import ProcessingTimes from './views/jobs/ProcessingTimes.jsx'; import ProcessingTimes from './views/jobs/ProcessingTimes.jsx';
import ListingManagement from './views/listings/management/ListingManagement.jsx'; import WatchlistManagement from './views/listings/management/WatchlistManagement.jsx';
export default function FredyApp() { export default function FredyApp() {
const actions = useActions(); const actions = useActions();
@@ -35,6 +35,7 @@ export default function FredyApp() {
async function init() { async function init() {
await actions.user.getCurrentUser(); await actions.user.getCurrentUser();
if (!needsLogin()) { if (!needsLogin()) {
await actions.features.getFeatures();
await actions.provider.getProvider(); await actions.provider.getProvider();
await actions.jobs.getJobs(); await actions.jobs.getJobs();
await actions.jobs.getProcessingTimes(); await actions.jobs.getProcessingTimes();
@@ -92,7 +93,7 @@ export default function FredyApp() {
<Route path="/jobs/insights/:jobId" element={<JobInsight />} /> <Route path="/jobs/insights/:jobId" element={<JobInsight />} />
<Route path="/jobs" element={<Jobs />} /> <Route path="/jobs" element={<Jobs />} />
<Route path="/listings" element={<Listings />} /> <Route path="/listings" element={<Listings />} />
<Route path="/listingManagement" element={<ListingManagement />} /> <Route path="/watchlistManagement" element={<WatchlistManagement />} />
{/* Permission-aware routes */} {/* Permission-aware routes */}
<Route <Route

View File

@@ -7,6 +7,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import './Navigate.less'; import './Navigate.less';
import { useScreenWidth } from '../../hooks/screenWidth.js'; import { useScreenWidth } from '../../hooks/screenWidth.js';
import { useFeature } from '../../hooks/featureHook.js';
export default function Navigation({ isAdmin }) { export default function Navigation({ isAdmin }) {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -14,6 +15,7 @@ export default function Navigation({ isAdmin }) {
const width = useScreenWidth(); const width = useScreenWidth();
const collapsed = width <= 850; const collapsed = width <= 850;
const watchlistFeature = useFeature('WATCHLIST_MANAGEMENT') || false;
const items = [ const items = [
{ itemKey: '/jobs', text: 'Jobs', icon: <IconTerminal /> }, { itemKey: '/jobs', text: 'Jobs', icon: <IconTerminal /> },
@@ -21,15 +23,19 @@ export default function Navigation({ isAdmin }) {
]; ];
if (isAdmin) { if (isAdmin) {
const settingsItems = [
{ itemKey: '/users', text: 'User Management' },
{ itemKey: '/generalSettings', text: 'General Settings' },
];
if (watchlistFeature) {
settingsItems.push({ itemKey: '/watchlistManagement', text: 'Watchlist Management' });
}
items.push({ items.push({
itemKey: 'settings', itemKey: 'settings',
text: 'Settings', text: 'Settings',
icon: <IconSetting />, icon: <IconSetting />,
items: [ items: settingsItems,
{ itemKey: '/users', text: 'User Management' },
{ itemKey: '/listingManagement', text: 'Listing Management' },
{ itemKey: '/generalSettings', text: 'General Settings' },
],
}); });
} }

View File

@@ -24,6 +24,7 @@ import { format } from '../../../services/time/timeService.js';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import { xhrDelete, xhrPost } from '../../../services/xhr.js'; import { xhrDelete, xhrPost } from '../../../services/xhr.js';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useFeature } from '../../../hooks/featureHook.js';
const getColumns = (provider, setProviderFilter, jobs, setJobNameFilter) => { const getColumns = (provider, setProviderFilter, jobs, setJobNameFilter) => {
return [ return [
@@ -239,6 +240,7 @@ export default function ListingsTable() {
const jobs = useSelector((state) => state.jobs.jobs); const jobs = useSelector((state) => state.jobs.jobs);
const navigate = useNavigate(); const navigate = useNavigate();
const watchlistFeature = useFeature('WATCHLIST_MANAGEMENT') || false;
const actions = useActions(); const actions = useActions();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 10; const pageSize = 10;
@@ -350,14 +352,16 @@ export default function ListingsTable() {
placeholder="Search" placeholder="Search"
onChange={handleFilterChange} onChange={handleFilterChange}
/> />
<Button {watchlistFeature && (
className="listingsTable__setupButton" <Button
onClick={() => { className="listingsTable__setupButton"
navigate('/listingManagement'); onClick={() => {
}} navigate('/watchlistManagement');
> }}
Setup notification on listing changes >
</Button> Setup notifications on watchlist changes
</Button>
)}
<Table <Table
rowKey="id" rowKey="id"
empty={empty} empty={empty}

View File

@@ -0,0 +1,15 @@
import { useSelector } from '../services/state/store.js';
export function useFeature(name) {
const currentFeatureFlags = useSelector((state) => state.features);
if (Object.keys(currentFeatureFlags || {}).length === 0) {
return null;
}
if (currentFeatureFlags[name] == null) {
console.warn(`Feature flag with name ${name} is unknown.`);
return null;
}
return currentFeatureFlags[name];
}

View File

@@ -48,6 +48,16 @@ export const useFredyState = create(
} }
}, },
}, },
features: {
async getFeatures() {
try {
const response = await xhrGet('/api/features');
set((state) => ({ ...state.features, ...response.json }));
} catch (Exception) {
console.error('Error while trying to get resource for api/features. Error:', Exception);
}
},
},
provider: { provider: {
async getProvider() { async getProvider() {
try { try {
@@ -176,6 +186,7 @@ export const useFredyState = create(
page: 1, page: 1,
result: [], result: [],
}, },
features: {},
generalSettings: { settings: {} }, generalSettings: { settings: {} },
demoMode: { demoMode: false }, demoMode: { demoMode: false },
versionUpdate: {}, versionUpdate: {},
@@ -192,6 +203,7 @@ export const useFredyState = create(
versionUpdate: { ...effects.versionUpdate }, versionUpdate: { ...effects.versionUpdate },
listingsTable: { ...effects.listingsTable }, listingsTable: { ...effects.listingsTable },
provider: { ...effects.provider }, provider: { ...effects.provider },
features: { ...effects.features },
jobs: { ...effects.jobs }, jobs: { ...effects.jobs },
user: { ...effects.user }, user: { ...effects.user },
}; };

View File

@@ -5,7 +5,7 @@ import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui';
import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx'; import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx';
import Headline from '../../../components/headline/Headline.jsx'; import Headline from '../../../components/headline/Headline.jsx';
export default function ListingManagement() { export default function WatchlistManagement() {
const [notificationChooserVisible, setNotificationChooserVisible] = useState(false); const [notificationChooserVisible, setNotificationChooserVisible] = useState(false);
const [notificationAdapterData, setNotificationAdapterData] = useState([]); const [notificationAdapterData, setNotificationAdapterData] = useState([]);
//TODO: Set default //TODO: Set default
@@ -29,7 +29,7 @@ export default function ListingManagement() {
<Headline size={5} text="Notify me when:" style={{ marginTop: '1rem' }} /> <Headline size={5} text="Notify me when:" style={{ marginTop: '1rem' }} />
<Checkbox checked={activityChanges} onChange={(e) => setActivityChanges(e.target.checked)}> <Checkbox checked={activityChanges} onChange={(e) => setActivityChanges(e.target.checked)}>
Listing state changes Listing state changes (e.g. listing becomes inactive)
</Checkbox> </Checkbox>
<Checkbox checked={priceChanges} onChange={(e) => setPriceChanges(e.target.checked)}> <Checkbox checked={priceChanges} onChange={(e) => setPriceChanges(e.target.checked)}>
Listing price changes Listing price changes