mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
adding features
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
12
lib/api/routes/featureRouter.js
Normal file
12
lib/api/routes/featureRouter.js
Normal 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
9
lib/features.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const FEATURES = {
|
||||||
|
WATCHLIST_MANAGEMENT: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getFeatures() {
|
||||||
|
return {
|
||||||
|
...FEATURES,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
15
ui/src/hooks/featureHook.js
Normal file
15
ui/src/hooks/featureHook.js
Normal 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];
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user