Skip to content

Commit 33120eb

Browse files
committed
ability to share jobs with users
1 parent de2dd05 commit 33120eb

File tree

11 files changed

+215
-57
lines changed

11 files changed

+215
-57
lines changed

lib/api/routes/jobRouter.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,25 @@ function doesJobBelongsToUser(job, req) {
2424
jobRouter.get('/', async (req, res) => {
2525
const isUserAdmin = isAdmin(req);
2626
//show only the jobs which belongs to the user (or all of the user is an admin)
27-
res.body = jobStorage.getJobs().filter((job) => isUserAdmin || job.userId === req.session.currentUser);
27+
res.body = jobStorage
28+
.getJobs()
29+
.filter(
30+
(job) =>
31+
isUserAdmin || job.userId === req.session.currentUser || job.shared_with_user.includes(req.session.currentUser),
32+
)
33+
.map((job) => {
34+
return {
35+
...job,
36+
isOnlyShared:
37+
!isUserAdmin &&
38+
job.userId !== req.session.currentUser &&
39+
job.shared_with_user.includes(req.session.currentUser),
40+
};
41+
});
42+
2843
res.send();
2944
});
45+
3046
jobRouter.get('/processingTimes', async (req, res) => {
3147
res.body = {
3248
interval: config.interval,
@@ -41,8 +57,15 @@ jobRouter.post('/startAll', async (req, res) => {
4157
});
4258

4359
jobRouter.post('/', async (req, res) => {
44-
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled } = req.body;
60+
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled, shareWithUsers = [] } = req.body;
4561
try {
62+
let jobFromDb = jobStorage.getJob(jobId);
63+
64+
if (jobFromDb && !doesJobBelongsToUser(jobFromDb, req)) {
65+
res.send(new Error('You are trying to change a job that is not associated to your user.'));
66+
return;
67+
}
68+
4669
jobStorage.upsertJob({
4770
userId: req.session.currentUser,
4871
jobId,
@@ -51,13 +74,15 @@ jobRouter.post('/', async (req, res) => {
5174
blacklist,
5275
provider,
5376
notificationAdapter,
77+
shareWithUsers,
5478
});
5579
} catch (error) {
5680
res.send(new Error(error));
5781
logger.error(error);
5882
}
5983
res.send();
6084
});
85+
6186
jobRouter.delete('', async (req, res) => {
6287
const { jobId } = req.body;
6388
try {
@@ -92,4 +117,16 @@ jobRouter.put('/:jobId/status', async (req, res) => {
92117
}
93118
res.send();
94119
});
120+
121+
jobRouter.get('/shareableUserList', async (req, res) => {
122+
const currentUser = req.session.currentUser;
123+
const users = userStorage.getUsers(false);
124+
res.body = users
125+
.filter((user) => !user.isAdmin && user.id !== currentUser)
126+
.map((user) => ({
127+
id: user.id,
128+
name: user.username,
129+
}));
130+
res.send();
131+
});
95132
export { jobRouter };

lib/api/routes/userRoute.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ function checkIfUserToBeRemovedIsLoggedIn(userIdToBeRemoved, req) {
1111
return req.session.currentUser === userIdToBeRemoved;
1212
}
1313
const nullOrEmpty = (str) => str == null || str.length === 0;
14+
1415
userRouter.get('/', async (req, res) => {
1516
res.body = userStorage.getUsers(false);
1617
res.send();
1718
});
19+
1820
userRouter.get('/:userId', async (req, res) => {
1921
const { userId } = req.params;
2022
res.body = userStorage.getUser(userId);

lib/services/storage/jobStorage.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ import { toJson, fromJson } from '../../utils.js';
1616
* @param {string} params.userId - Owner user id for inserts; preserved on updates.
1717
* @returns {void}
1818
*/
19-
export const upsertJob = ({ jobId, name, blacklist = [], enabled = true, provider, notificationAdapter, userId }) => {
19+
export const upsertJob = ({
20+
jobId,
21+
name,
22+
blacklist = [],
23+
enabled = true,
24+
provider,
25+
notificationAdapter,
26+
userId,
27+
shareWithUsers = [],
28+
}) => {
2029
const id = jobId || nanoid();
2130
const existing = SqliteConnection.query(`SELECT id, user_id FROM jobs WHERE id = @id LIMIT 1`, { id })[0];
2231
const ownerId = existing ? existing.user_id : userId;
@@ -27,28 +36,31 @@ export const upsertJob = ({ jobId, name, blacklist = [], enabled = true, provide
2736
name = @name,
2837
blacklist = @blacklist,
2938
provider = @provider,
30-
notification_adapter = @notification_adapter
39+
notification_adapter = @notification_adapter,
40+
shared_with_user = @shareWithUsers
3141
WHERE id = @id`,
3242
{
3343
id,
3444
enabled: enabled ? 1 : 0,
3545
name: name ?? null,
3646
blacklist: toJson(blacklist ?? []),
47+
shareWithUsers: toJson(shareWithUsers ?? []),
3748
provider: toJson(provider ?? []),
3849
notification_adapter: toJson(notificationAdapter ?? []),
3950
},
4051
);
4152
} else {
4253
SqliteConnection.execute(
43-
`INSERT INTO jobs (id, user_id, enabled, name, blacklist, provider, notification_adapter)
44-
VALUES (@id, @user_id, @enabled, @name, @blacklist, @provider, @notification_adapter)`,
54+
`INSERT INTO jobs (id, user_id, enabled, name, blacklist, provider, notification_adapter, shared_with_user)
55+
VALUES (@id, @user_id, @enabled, @name, @blacklist, @provider, @notification_adapter, @shareWithUsers)`,
4556
{
4657
id,
4758
user_id: ownerId,
4859
enabled: enabled ? 1 : 0,
4960
name: name ?? null,
5061
blacklist: toJson(blacklist ?? []),
5162
provider: toJson(provider ?? []),
63+
shareWithUsers: toJson(shareWithUsers ?? []),
5264
notification_adapter: toJson(notificationAdapter ?? []),
5365
},
5466
);
@@ -129,6 +141,7 @@ export const getJobs = () => {
129141
j.name,
130142
j.blacklist,
131143
j.provider,
144+
j.shared_with_user,
132145
j.notification_adapter AS notificationAdapter,
133146
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id) AS numberOfFoundListings
134147
FROM jobs j
@@ -139,6 +152,7 @@ export const getJobs = () => {
139152
enabled: !!row.enabled,
140153
blacklist: fromJson(row.blacklist, []),
141154
provider: fromJson(row.provider, []),
155+
shared_with_user: fromJson(row.shared_with_user, []),
142156
notificationAdapter: fromJson(row.notificationAdapter, []),
143157
}));
144158
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Migration: Adding a new table to store if somebody "watches" (a.k.a favorite) a listing
2+
3+
export function up(db) {
4+
db.exec(`
5+
ALTER TABLE jobs ADD COLUMN shared_with_user jsonb DEFAULT '[]'
6+
`);
7+
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fredy",
3-
"version": "14.1.1",
3+
"version": "14.2.0",
44
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
55
"scripts": {
66
"prepare": "husky",
@@ -85,7 +85,7 @@
8585
"react-router": "7.9.3",
8686
"react-router-dom": "7.9.3",
8787
"restana": "5.1.0",
88-
"semver": "^7.7.2",
88+
"semver": "^7.7.3",
8989
"serve-static": "2.2.0",
9090
"slack": "11.0.2",
9191
"vite": "7.1.9",
@@ -98,13 +98,13 @@
9898
"@babel/preset-env": "7.28.3",
9999
"@babel/preset-react": "7.27.1",
100100
"chai": "6.2.0",
101-
"eslint": "9.36.0",
101+
"eslint": "9.37.0",
102102
"eslint-config-prettier": "10.1.8",
103103
"eslint-plugin-react": "7.37.5",
104104
"esmock": "2.7.3",
105105
"history": "5.3.0",
106106
"husky": "9.1.7",
107-
"less": "4.4.1",
107+
"less": "4.4.2",
108108
"lint-staged": "16.2.3",
109109
"mocha": "11.7.4",
110110
"nodemon": "^3.1.10",

ui/src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default function FredyApp() {
3737
await actions.provider.getProvider();
3838
await actions.jobs.getJobs();
3939
await actions.jobs.getProcessingTimes();
40+
await actions.jobs.getSharableUserList();
4041
await actions.notificationAdapter.getAdapter();
4142
await actions.generalSettings.getGeneralSettings();
4243
await actions.versionUpdate.getVersionUpdate();

ui/src/components/table/JobTable.jsx

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import { Button, Empty, Table, Switch, Popover } from '@douyinfe/semi-ui';
4-
import { IconDelete, IconDescend2, IconEdit, IconHistogram } from '@douyinfe/semi-icons';
4+
import { IconAlertTriangle, IconDelete, IconDescend2, IconEdit, IconHistogram } from '@douyinfe/semi-icons';
55
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
66

77
import './JobTable.less';
@@ -33,12 +33,38 @@ export default function JobTable({
3333
title: '',
3434
dataIndex: '',
3535
render: (job) => {
36-
return <Switch onChange={(checked) => onJobStatusChanged(job.id, checked)} checked={job.enabled} />;
36+
return (
37+
<Switch
38+
onChange={(checked) => onJobStatusChanged(job.id, checked)}
39+
checked={job.enabled}
40+
disabled={job.isOnlyShared}
41+
/>
42+
);
3743
},
3844
},
3945
{
4046
title: 'Name',
4147
dataIndex: 'name',
48+
render: (name, job) => {
49+
if (job.isOnlyShared) {
50+
return (
51+
<Popover
52+
content={getPopoverContent(
53+
'This job has been shared with you by another user, therefor it is read-only.',
54+
)}
55+
>
56+
<div style={{ display: 'flex', gap: '.3rem' }}>
57+
<div style={{ color: 'rgba(var(--semi-yellow-7), 1)' }}>
58+
<IconAlertTriangle />
59+
</div>
60+
{name}
61+
</div>
62+
</Popover>
63+
);
64+
} else {
65+
return name;
66+
}
67+
},
4268
},
4369
{
4470
title: 'Listings',
@@ -48,14 +74,14 @@ export default function JobTable({
4874
},
4975
},
5076
{
51-
title: 'Providers',
77+
title: 'Provider',
5278
dataIndex: 'provider',
5379
render: (value) => {
5480
return value.length || 0;
5581
},
5682
},
5783
{
58-
title: 'Notification adapters',
84+
title: 'Notification Adapter',
5985
dataIndex: 'notificationAdapter',
6086
render: (value) => {
6187
return value.length || 0;
@@ -68,16 +94,36 @@ export default function JobTable({
6894
return (
6995
<div className="interactions">
7096
<Popover content={getPopoverContent('Job Insights')}>
71-
<Button type="primary" icon={<IconHistogram />} onClick={() => onJobInsight(job.id)} />
97+
<Button
98+
type="primary"
99+
icon={<IconHistogram />}
100+
disabled={job.isOnlyShared}
101+
onClick={() => onJobInsight(job.id)}
102+
/>
72103
</Popover>
73104
<Popover content={getPopoverContent('Edit a Job')}>
74-
<Button type="secondary" icon={<IconEdit />} onClick={() => onJobEdit(job.id)} />
105+
<Button
106+
type="secondary"
107+
icon={<IconEdit />}
108+
disabled={job.isOnlyShared}
109+
onClick={() => onJobEdit(job.id)}
110+
/>
75111
</Popover>
76112
<Popover content={getPopoverContent('Delete all found Listings of this Job')}>
77-
<Button type="danger" icon={<IconDescend2 />} onClick={() => onListingRemoval(job.id)} />
113+
<Button
114+
type="danger"
115+
icon={<IconDescend2 />}
116+
disabled={job.isOnlyShared}
117+
onClick={() => onListingRemoval(job.id)}
118+
/>
78119
</Popover>
79120
<Popover content={getPopoverContent('Delete Job')}>
80-
<Button type="danger" icon={<IconDelete />} onClick={() => onJobRemoval(job.id)} />
121+
<Button
122+
type="danger"
123+
icon={<IconDelete />}
124+
disabled={job.isOnlyShared}
125+
onClick={() => onJobRemoval(job.id)}
126+
/>
81127
</Popover>
82128
</div>
83129
);

ui/src/components/table/listings/ListingsFilter.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export default function ListingsFilter({ onWatchListFilter, onActivityFilter, on
2626
{jobs != null &&
2727
jobs.length > 0 &&
2828
jobs.map((job) => {
29-
return <Select.Option value={job.id}>{job.name}</Select.Option>;
29+
return (
30+
<Select.Option value={job.id} key={job.id}>
31+
{job.name}
32+
</Select.Option>
33+
);
3034
})}
3135
</Select>
3236
</Descriptions.Item>
@@ -35,7 +39,11 @@ export default function ListingsFilter({ onWatchListFilter, onActivityFilter, on
3539
{provider != null &&
3640
provider.length > 0 &&
3741
provider.map((prov) => {
38-
return <Select.Option value={prov.id}>{prov.name}</Select.Option>;
42+
return (
43+
<Select.Option value={prov.id} key={prov.id}>
44+
{prov.name}
45+
</Select.Option>
46+
);
3947
})}
4048
</Select>
4149
</Descriptions.Item>

ui/src/services/state/store.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export const useFredyState = create(
6767
console.error(`Error while trying to get resource for api/jobs. Error:`, Exception);
6868
}
6969
},
70+
async getSharableUserList() {
71+
try {
72+
const response = await xhrGet('/api/jobs/shareableUserList');
73+
set((state) => ({ jobs: { ...state.jobs, shareableUserList: Object.freeze(response.json) } }));
74+
} catch (Exception) {
75+
console.error(`Error while trying to get resource for api/jobs. Error:`, Exception);
76+
}
77+
},
7078
async getProcessingTimes() {
7179
try {
7280
const response = await xhrGet('/api/jobs/processingTimes');
@@ -172,7 +180,7 @@ export const useFredyState = create(
172180
demoMode: { demoMode: false },
173181
versionUpdate: {},
174182
provider: [],
175-
jobs: { jobs: [], insights: {}, processingTimes: {} },
183+
jobs: { jobs: [], insights: {}, processingTimes: {}, shareableUserList: [] },
176184
user: { users: [], currentUser: null },
177185
};
178186

0 commit comments

Comments
 (0)