diff --git a/lib/api/api.js b/lib/api/api.js index 66c41b7..da65b16 100644 --- a/lib/api/api.js +++ b/lib/api/api.js @@ -16,6 +16,7 @@ import { demoRouter } from './routes/demoRouter.js'; import logger from '../services/logger.js'; import { listingsRouter } from './routes/listingsRouter.js'; import { getSettings } from '../services/storage/settingsStorage.js'; +import { featureRouter } from './routes/featureRouter.js'; const service = restana(); const staticService = files(path.join(getDirName(), '../ui/public')); const PORT = (await getSettings()).port || 9998; @@ -39,6 +40,7 @@ service.use('/api/version', versionRouter); service.use('/api/jobs', jobRouter); service.use('/api/login', loginRouter); service.use('/api/listings', listingsRouter); +service.use('/api/features', featureRouter); //this route is unsecured intentionally as it is being queried from the login page service.use('/api/demo', demoRouter); diff --git a/lib/api/routes/featureRouter.js b/lib/api/routes/featureRouter.js new file mode 100644 index 0000000..2dbeba2 --- /dev/null +++ b/lib/api/routes/featureRouter.js @@ -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 }; diff --git a/lib/features.js b/lib/features.js new file mode 100644 index 0000000..fa3785e --- /dev/null +++ b/lib/features.js @@ -0,0 +1,9 @@ +const FEATURES = { + WATCHLIST_MANAGEMENT: false, +}; + +export default function getFeatures() { + return { + ...FEATURES, + }; +} diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 317e1f7..423433d 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -21,7 +21,7 @@ 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'; -import ListingManagement from './views/listings/management/ListingManagement.jsx'; +import WatchlistManagement from './views/listings/management/WatchlistManagement.jsx'; export default function FredyApp() { const actions = useActions(); @@ -35,6 +35,7 @@ export default function FredyApp() { async function init() { await actions.user.getCurrentUser(); if (!needsLogin()) { + await actions.features.getFeatures(); await actions.provider.getProvider(); await actions.jobs.getJobs(); await actions.jobs.getProcessingTimes(); @@ -92,7 +93,7 @@ export default function FredyApp() { } /> } /> } /> - } /> + } /> {/* Permission-aware routes */} }, @@ -21,15 +23,19 @@ export default function Navigation({ 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({ itemKey: 'settings', text: 'Settings', icon: , - items: [ - { itemKey: '/users', text: 'User Management' }, - { itemKey: '/listingManagement', text: 'Listing Management' }, - { itemKey: '/generalSettings', text: 'General Settings' }, - ], + items: settingsItems, }); } diff --git a/ui/src/components/table/listings/ListingsTable.jsx b/ui/src/components/table/listings/ListingsTable.jsx index f00db25..f29f6cc 100644 --- a/ui/src/components/table/listings/ListingsTable.jsx +++ b/ui/src/components/table/listings/ListingsTable.jsx @@ -24,6 +24,7 @@ import { format } from '../../../services/time/timeService.js'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { xhrDelete, xhrPost } from '../../../services/xhr.js'; import { useNavigate } from 'react-router-dom'; +import { useFeature } from '../../../hooks/featureHook.js'; const getColumns = (provider, setProviderFilter, jobs, setJobNameFilter) => { return [ @@ -239,6 +240,7 @@ export default function ListingsTable() { const jobs = useSelector((state) => state.jobs.jobs); const navigate = useNavigate(); + const watchlistFeature = useFeature('WATCHLIST_MANAGEMENT') || false; const actions = useActions(); const [page, setPage] = useState(1); const pageSize = 10; @@ -350,14 +352,16 @@ export default function ListingsTable() { placeholder="Search" onChange={handleFilterChange} /> - + {watchlistFeature && ( + + )} 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]; +} diff --git a/ui/src/services/state/store.js b/ui/src/services/state/store.js index 9ee44a4..3d2fd81 100644 --- a/ui/src/services/state/store.js +++ b/ui/src/services/state/store.js @@ -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: { async getProvider() { try { @@ -176,6 +186,7 @@ export const useFredyState = create( page: 1, result: [], }, + features: {}, generalSettings: { settings: {} }, demoMode: { demoMode: false }, versionUpdate: {}, @@ -192,6 +203,7 @@ export const useFredyState = create( versionUpdate: { ...effects.versionUpdate }, listingsTable: { ...effects.listingsTable }, provider: { ...effects.provider }, + features: { ...effects.features }, jobs: { ...effects.jobs }, user: { ...effects.user }, }; diff --git a/ui/src/views/listings/management/ListingManagement.jsx b/ui/src/views/listings/management/WatchlistManagement.jsx similarity index 95% rename from ui/src/views/listings/management/ListingManagement.jsx rename to ui/src/views/listings/management/WatchlistManagement.jsx index 0023e0c..b80820f 100644 --- a/ui/src/views/listings/management/ListingManagement.jsx +++ b/ui/src/views/listings/management/WatchlistManagement.jsx @@ -5,7 +5,7 @@ import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui'; import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx'; import Headline from '../../../components/headline/Headline.jsx'; -export default function ListingManagement() { +export default function WatchlistManagement() { const [notificationChooserVisible, setNotificationChooserVisible] = useState(false); const [notificationAdapterData, setNotificationAdapterData] = useState([]); //TODO: Set default @@ -29,7 +29,7 @@ export default function ListingManagement() { setActivityChanges(e.target.checked)}> - Listing state changes + Listing state changes (e.g. listing becomes inactive) setPriceChanges(e.target.checked)}> Listing price changes