mirror of
https://github.com/KSJaay/Lunalytics.git
synced 2026-01-06 11:40:21 -06:00
Allows users to sort monitors in any order
This commit is contained in:
33
app/components/home/menu.tsx
Normal file
33
app/components/home/menu.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Dropdown } from '@lunalytics/ui';
|
||||
import { FaEllipsisVertical, FaSort } from 'react-icons/fa6';
|
||||
import NavigationReorderModal from '../modal/navigation/reorder';
|
||||
import useContextStore from '../../context';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const HomeMenu = () => {
|
||||
const {
|
||||
modalStore: { openModal, closeModal },
|
||||
} = useContextStore();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
id: 'customize-dashboard',
|
||||
text: 'Reorder Monitors',
|
||||
type: 'item',
|
||||
icon: (<FaSort />) as React.ReactNode,
|
||||
onClick: () => {
|
||||
openModal(<NavigationReorderModal closeModal={closeModal} />);
|
||||
},
|
||||
},
|
||||
]}
|
||||
hideIcon
|
||||
position="top"
|
||||
>
|
||||
<FaEllipsisVertical size={20} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(HomeMenu);
|
||||
@@ -7,6 +7,7 @@ import useContextStore from '../../context';
|
||||
import PillCircle from '../navigation/PillCircle';
|
||||
import HomeMonitorsListContext from './context';
|
||||
import type { ContextMonitorProps } from '../../types/context/global';
|
||||
import { getMonitorsInOrder } from '../modal/navigation/reorder';
|
||||
|
||||
const HomeMonitorsList = ({
|
||||
monitors = [],
|
||||
@@ -14,71 +15,73 @@ const HomeMonitorsList = ({
|
||||
monitors: ContextMonitorProps[];
|
||||
}) => {
|
||||
const {
|
||||
globalStore: { activeMonitor, setActiveMonitor },
|
||||
userStore: { user },
|
||||
globalStore: { activeMonitor, setActiveMonitor, getMonitor },
|
||||
} = useContextStore();
|
||||
|
||||
const monitorsList = monitors
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((monitor) => {
|
||||
const classes = classNames('item', {
|
||||
'item-active': activeMonitor?.monitorId === monitor.monitorId,
|
||||
});
|
||||
const monitorsList = getMonitorsInOrder(
|
||||
monitors,
|
||||
user?.settings?.monitorsList
|
||||
).map((item) => {
|
||||
const monitor = getMonitor(item.monitorId);
|
||||
|
||||
let heartbeats: string[] = monitor.heartbeats
|
||||
.map((heartbeat) => {
|
||||
const downColor = monitor.paused
|
||||
? 'var(--red-900)'
|
||||
: 'var(--red-800)';
|
||||
const upColor = monitor.paused
|
||||
? 'var(--green-900)'
|
||||
: 'var(--green-800)';
|
||||
const classes = classNames('item', {
|
||||
'item-active': activeMonitor?.monitorId === monitor.monitorId,
|
||||
});
|
||||
|
||||
return heartbeat.isDown ? downColor : upColor;
|
||||
})
|
||||
.splice(0, 12);
|
||||
let heartbeats: string[] = monitor.heartbeats
|
||||
.map((heartbeat) => {
|
||||
const downColor = monitor.paused ? 'var(--red-900)' : 'var(--red-800)';
|
||||
const upColor = monitor.paused
|
||||
? 'var(--green-900)'
|
||||
: 'var(--green-800)';
|
||||
|
||||
if (heartbeats.length < 12) {
|
||||
const length = 12 - heartbeats.length;
|
||||
const emptyHeartbeats = Array.from({ length }).map(
|
||||
() => 'var(--gray-500)'
|
||||
);
|
||||
return heartbeat.isDown ? downColor : upColor;
|
||||
})
|
||||
.splice(0, 12);
|
||||
|
||||
heartbeats = heartbeats.concat(emptyHeartbeats);
|
||||
}
|
||||
if (heartbeats.length < 12) {
|
||||
const length = 12 - heartbeats.length;
|
||||
const emptyHeartbeats = Array.from({ length }).map(
|
||||
() => 'var(--gray-500)'
|
||||
);
|
||||
|
||||
const iconUrl = monitor.icon?.url || '/logo.svg';
|
||||
heartbeats = heartbeats.concat(emptyHeartbeats);
|
||||
}
|
||||
|
||||
return (
|
||||
<HomeMonitorsListContext
|
||||
monitorId={monitor.monitorId}
|
||||
key={monitor.monitorId}
|
||||
const iconUrl = monitor.icon?.url || '/logo.svg';
|
||||
|
||||
return (
|
||||
<HomeMonitorsListContext
|
||||
monitorId={monitor.monitorId}
|
||||
key={monitor.monitorId}
|
||||
>
|
||||
<div
|
||||
className={classes}
|
||||
onClick={() => setActiveMonitor(monitor.monitorId)}
|
||||
>
|
||||
<div
|
||||
className={classes}
|
||||
onClick={() => setActiveMonitor(monitor.monitorId)}
|
||||
>
|
||||
<div className="content">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<img src={iconUrl} style={{ width: '35px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div>{monitor.name}</div>
|
||||
<span>{monitor.url}</span>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<img src={iconUrl} style={{ width: '35px' }} />
|
||||
</div>
|
||||
<div className="pill-container">
|
||||
<PillCircle pills={heartbeats} />
|
||||
<div>
|
||||
<div>{monitor.name}</div>
|
||||
<span>{monitor.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
</HomeMonitorsListContext>
|
||||
);
|
||||
});
|
||||
<div className="pill-container">
|
||||
<PillCircle pills={heartbeats} />
|
||||
</div>
|
||||
</div>
|
||||
</HomeMonitorsListContext>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="navigation-monitor-items">{monitorsList}</div>;
|
||||
};
|
||||
|
||||
185
app/components/modal/navigation/reorder.tsx
Normal file
185
app/components/modal/navigation/reorder.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
// import dependencies
|
||||
import { createSwapy } from 'swapy';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button, Modal } from '@lunalytics/ui';
|
||||
import { PiDotsSixVerticalBold } from 'react-icons/pi';
|
||||
|
||||
// import local files
|
||||
import useContextStore from '../../../context';
|
||||
import { createPostRequest } from '../../../services/axios';
|
||||
|
||||
interface NavigationReorderModalProps {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const getMonitorsInOrder = (
|
||||
allMonitors: Array<any>,
|
||||
monitorsList: Array<{
|
||||
type: 'monitor' | 'folder';
|
||||
isHidden: boolean;
|
||||
monitorId: string;
|
||||
}>
|
||||
) => {
|
||||
let userMonitors = monitorsList || [];
|
||||
|
||||
allMonitors.forEach((monitor) => {
|
||||
if (!userMonitors.find((m) => m.monitorId === monitor.monitorId)) {
|
||||
userMonitors.push({
|
||||
type: 'monitor',
|
||||
isHidden: false,
|
||||
monitorId: monitor.monitorId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return userMonitors;
|
||||
};
|
||||
|
||||
const NavigationReorderModal = ({
|
||||
closeModal,
|
||||
}: NavigationReorderModalProps) => {
|
||||
const {
|
||||
globalStore: { allMonitors },
|
||||
userStore: { user, updateUsingKey },
|
||||
} = useContextStore();
|
||||
|
||||
const swapy = useRef<any>(null);
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (container.current) {
|
||||
swapy.current = createSwapy(container.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
swapy.current?.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const monitorOrder = getMonitorsInOrder(
|
||||
allMonitors,
|
||||
user?.settings?.monitorsList
|
||||
);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const order = swapy.current
|
||||
.slotItemMap()
|
||||
.asArray.map((obj: any) =>
|
||||
monitorOrder.find((m: any) => m.monitorId === obj.item)
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
await createPostRequest('/api/user/update/settings', {
|
||||
settings: {
|
||||
...user?.settings,
|
||||
monitorsList: order,
|
||||
},
|
||||
});
|
||||
|
||||
updateUsingKey('settings', {
|
||||
...user.settings,
|
||||
monitorsList: order,
|
||||
});
|
||||
|
||||
toast.success('Monitor order updated successfully');
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={closeModal}
|
||||
size="sm"
|
||||
height="sm"
|
||||
title="Reorder Monitors"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
color="red"
|
||||
variant="flat"
|
||||
id="home-reorder-cancel-button"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
variant="flat"
|
||||
id="home-reorder-confirm-button"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}
|
||||
ref={container}
|
||||
>
|
||||
{monitorOrder.map((item) => {
|
||||
const monitor = allMonitors.find(
|
||||
(m) => m.monitorId === item.monitorId
|
||||
);
|
||||
|
||||
if (!monitor) return null;
|
||||
|
||||
return (
|
||||
<div data-swapy-slot={item.monitorId} key={item.monitorId}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
border: '2px solid var(--accent-700)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'var(--accent-800)',
|
||||
}}
|
||||
data-swapy-item={item.monitorId}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '6px',
|
||||
fontSize: 'var(--font-lg)',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={monitor.icon.url}
|
||||
alt={monitor.name}
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
/>
|
||||
<div>{monitor.name}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{/* {monitorOrder.find((m) => m.monitorId === item.monitorId)
|
||||
?.isHidden ? (
|
||||
<IoMdEyeOff size={24} />
|
||||
) : (
|
||||
<IoMdEye size={24} />
|
||||
)} */}
|
||||
<PiDotsSixVerticalBold
|
||||
style={{ width: '24px', height: '24px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
NavigationReorderModal.displayName = 'NavigationReorderModal';
|
||||
|
||||
export default observer(NavigationReorderModal);
|
||||
@@ -6,7 +6,6 @@ import { Button } from '@lunalytics/ui';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// import { FaEllipsisVertical } from 'react-icons/fa6';
|
||||
|
||||
// import local files
|
||||
import useContextStore from '../context';
|
||||
@@ -22,6 +21,7 @@ import MonitorConfigureModal from '../components/modal/monitor/configure';
|
||||
import NavigationMonitorInfo from '../components/navigation/info/monitor';
|
||||
import useScreenSize from '../hooks/useScreenSize';
|
||||
import { filterData } from '../../shared/utils/search';
|
||||
import HomeMenu from '../components/home/menu';
|
||||
|
||||
const Home = () => {
|
||||
const {
|
||||
@@ -96,9 +96,7 @@ const Home = () => {
|
||||
>
|
||||
{t('home.monitor.add')}
|
||||
</Button>
|
||||
{/* <div className="monitor-left-menu">
|
||||
<FaEllipsisVertical size={20} />
|
||||
</div> */}
|
||||
<HomeMenu />
|
||||
</div>
|
||||
}
|
||||
rightChildren={
|
||||
|
||||
2
app/types/constant/notifications.d.ts
vendored
2
app/types/constant/notifications.d.ts
vendored
@@ -8,7 +8,7 @@ export interface NotificationInputInputType {
|
||||
| 'group'
|
||||
| 'textarea'
|
||||
| 'empty';
|
||||
isDataField: boolean;
|
||||
isDataField?: boolean;
|
||||
key: string;
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
|
||||
9
app/types/context/user.d.ts
vendored
9
app/types/context/user.d.ts
vendored
@@ -1,3 +1,11 @@
|
||||
export interface UserSettings {
|
||||
monitorsList?: Array<{
|
||||
type: 'monitor' | 'folder';
|
||||
isHidden: boolean;
|
||||
monitorId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ContextUserProps {
|
||||
email: string;
|
||||
displayName: string;
|
||||
@@ -6,4 +14,5 @@ export interface ContextUserProps {
|
||||
permission: number;
|
||||
createdAt: string;
|
||||
isOwner: boolean;
|
||||
settings: UserSettings;
|
||||
}
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lunalytics",
|
||||
"version": "0.10.14",
|
||||
"version": "0.10.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lunalytics",
|
||||
"version": "0.10.14",
|
||||
"version": "0.10.16",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "6.3.1",
|
||||
@@ -9172,9 +9172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lunalytics",
|
||||
"version": "0.10.14",
|
||||
"version": "0.10.16",
|
||||
"description": "Open source Node.js server/website monitoring tool",
|
||||
"private": true,
|
||||
"author": "KSJaay <ksjaay@gmail.com>",
|
||||
|
||||
22
scripts/migrations/0-10-16.js
Normal file
22
scripts/migrations/0-10-16.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// import local files
|
||||
import SQLite from '../../server/database/sqlite/setup.js';
|
||||
import logger from '../../server/utils/logger.js';
|
||||
|
||||
const infomation = {
|
||||
title: 'Adds settings column to user table',
|
||||
description: 'Adds settings column to user table to store user preferences',
|
||||
version: '0.10.16',
|
||||
};
|
||||
|
||||
const migrate = async () => {
|
||||
const client = await SQLite.connect();
|
||||
|
||||
await client.schema.alterTable('user', (table) => {
|
||||
table.jsonb('settings').defaultTo(JSON.stringify({}));
|
||||
});
|
||||
|
||||
logger.info('Migrations', { message: '0.10.16 has been applied' });
|
||||
return;
|
||||
};
|
||||
|
||||
export { infomation, migrate };
|
||||
@@ -12,6 +12,7 @@ import { migrate as migrateMonitor } from './0-9-5.js';
|
||||
import { migrate as migrateMonitorJson } from './0-9-7.js';
|
||||
import { migrate as migrateUiOverhaul } from './0-10-0.js';
|
||||
import { migrate as migrateMonitorParent } from './0-10-13.js';
|
||||
import { migrate as migrateUserSettings } from './0-10-16.js';
|
||||
|
||||
const migrationList = {
|
||||
'0.4.0': migrateTcpUpdate,
|
||||
@@ -27,6 +28,7 @@ const migrationList = {
|
||||
'0.9.7': migrateMonitorJson,
|
||||
'0.10.0': migrateUiOverhaul,
|
||||
'0.10.13': migrateMonitorParent,
|
||||
'0.10.16': migrateUserSettings,
|
||||
};
|
||||
|
||||
export default migrationList;
|
||||
|
||||
@@ -79,7 +79,7 @@ export const registerSsoUser = async (data) => {
|
||||
};
|
||||
|
||||
export const getUserByEmail = async (email) => {
|
||||
return SQLite.client('user')
|
||||
let user = await SQLite.client('user')
|
||||
.where({ email })
|
||||
.select(
|
||||
'email',
|
||||
@@ -89,25 +89,20 @@ export const getUserByEmail = async (email) => {
|
||||
'permission',
|
||||
'createdAt',
|
||||
'isOwner',
|
||||
'sso'
|
||||
'sso',
|
||||
'settings'
|
||||
)
|
||||
.first();
|
||||
};
|
||||
|
||||
export const emailExists = async (email) => {
|
||||
return SQLite.client('user')
|
||||
.where({ email })
|
||||
.select(
|
||||
'email',
|
||||
'displayName',
|
||||
'avatar',
|
||||
'isVerified',
|
||||
'permission',
|
||||
'createdAt',
|
||||
'isOwner',
|
||||
'sso'
|
||||
)
|
||||
.first();
|
||||
if (user && user.settings) {
|
||||
try {
|
||||
user.settings = JSON.parse(user.settings);
|
||||
} catch {
|
||||
user.settings = {};
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export const emailIsOwner = async (email) => {
|
||||
@@ -115,7 +110,7 @@ export const emailIsOwner = async (email) => {
|
||||
};
|
||||
|
||||
export const ownerExists = async () => {
|
||||
return SQLite?.client('user')
|
||||
let user = await SQLite?.client('user')
|
||||
.where({ permission: oldPermsToFlags[1] })
|
||||
.select(
|
||||
'email',
|
||||
@@ -125,9 +120,20 @@ export const ownerExists = async () => {
|
||||
'permission',
|
||||
'createdAt',
|
||||
'isOwner',
|
||||
'sso'
|
||||
'sso',
|
||||
'settings'
|
||||
)
|
||||
.first();
|
||||
|
||||
if (user && user.settings) {
|
||||
try {
|
||||
user.settings = JSON.parse(user.settings);
|
||||
} catch {
|
||||
user.settings = {};
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export const getUserPasswordUsingEmail = async (email) => {
|
||||
@@ -211,6 +217,12 @@ export const updateUserPassword = (email, password) => {
|
||||
.update({ password: hashedPassword });
|
||||
};
|
||||
|
||||
export const updateUserSettings = (email, settings) => {
|
||||
return SQLite.client('user')
|
||||
.where({ email })
|
||||
.update({ settings: JSON.stringify(settings) });
|
||||
};
|
||||
|
||||
export const resetDemoUser = async () => {
|
||||
const demoUser = await SQLite.client('user').where({ email: 'demo' }).first();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const userTable = async (client) => {
|
||||
table.boolean('sso').defaultTo(0);
|
||||
table.integer('permission').defaultTo(oldPermsToFlags[4]);
|
||||
table.datetime('createdAt');
|
||||
table.jsonb('settings').defaultTo(JSON.stringify({}));
|
||||
|
||||
table.index('email');
|
||||
table.index('isVerified');
|
||||
|
||||
24
server/middleware/user/update/settings.js
Normal file
24
server/middleware/user/update/settings.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { updateUserSettings } from '../../../database/queries/user.js';
|
||||
import { handleError } from '../../../utils/errors.js';
|
||||
|
||||
const userUpdateSettings = async (request, response) => {
|
||||
try {
|
||||
const { settings } = request.body;
|
||||
|
||||
if (
|
||||
typeof settings !== 'object' ||
|
||||
settings === null ||
|
||||
Array.isArray(settings)
|
||||
) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
await updateUserSettings(response.locals.user.email, settings);
|
||||
|
||||
return response.sendStatus(200);
|
||||
} catch (error) {
|
||||
handleError(error, response);
|
||||
}
|
||||
};
|
||||
|
||||
export default userUpdateSettings;
|
||||
@@ -19,6 +19,7 @@ import { hasRequiredPermission } from '../middleware/user/hasPermission.js';
|
||||
import deleteConnectionMiddleware from '../middleware/user/connections/delete.js';
|
||||
import getAllConnectionMiddleware from '../middleware/user/connections/getAll.js';
|
||||
import createConnectionMiddleware from '../middleware/user/connections/create.js';
|
||||
import userUpdateSettings from '../middleware/user/update/settings.js';
|
||||
|
||||
router.get('/', fetchUserMiddleware);
|
||||
|
||||
@@ -38,6 +39,8 @@ router.post('/update/password', userUpdatePassword);
|
||||
|
||||
router.post('/update/avatar', userUpdateAvatar);
|
||||
|
||||
router.post('/update/settings', userUpdateSettings);
|
||||
|
||||
router.get('/team', teamMembersListMiddleware);
|
||||
|
||||
router.get('/connections', getAllConnectionMiddleware);
|
||||
|
||||
Reference in New Issue
Block a user