Show currently active users in world in top bar
This commit is contained in:
@@ -3,13 +3,13 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { Button, ButtonGroup, ConfirmModal, Input, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { Button, ButtonGroup, ConfirmModal, Input, ToolbarButton, UsersIndicator, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { useTransformContext } from '../context/TransformContext';
|
||||
import { useCanvasPersistence } from '../hooks/useCanvasPersistence';
|
||||
import { updateMapTitle } from '../state/crdtSlice';
|
||||
import { selectPanelCount, selectViewport, selectMapTitle } from '../state/selectors';
|
||||
import { selectPanelCount, selectViewport, selectMapTitle, selectActiveUsers } from '../state/selectors';
|
||||
|
||||
interface ExploreMapToolbarProps {
|
||||
uid?: string;
|
||||
@@ -28,6 +28,7 @@ export function ExploreMapToolbar({ uid }: ExploreMapToolbarProps) {
|
||||
const panelCount = useSelector((state) => selectPanelCount(state.exploreMapCRDT));
|
||||
const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT));
|
||||
const mapTitle = useSelector((state) => selectMapTitle(state.exploreMapCRDT));
|
||||
const activeUsers = useSelector((state) => selectActiveUsers(state.exploreMapCRDT));
|
||||
|
||||
useEffect(() => {
|
||||
if (mapTitle) {
|
||||
@@ -185,6 +186,11 @@ export function ExploreMapToolbar({ uid }: ExploreMapToolbarProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.toolbarSection}>
|
||||
{activeUsers.length > 0 && (
|
||||
<div className={styles.activeUsersContainer}>
|
||||
<UsersIndicator users={activeUsers} limit={5} />
|
||||
</div>
|
||||
)}
|
||||
<ButtonGroup>
|
||||
<ToolbarButton
|
||||
icon="save"
|
||||
@@ -276,5 +282,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: theme.colors.text.secondary,
|
||||
fontStyle: 'italic',
|
||||
}),
|
||||
activeUsersContainer: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -298,6 +298,48 @@ export const selectCRDTStateJSON = (state: ExploreMapCRDTState): string | undefi
|
||||
return state.crdtStateJSON;
|
||||
};
|
||||
|
||||
/**
|
||||
* Select active users from cursors
|
||||
* Groups by userId and filters out stale cursors (not updated in last 15 minutes)
|
||||
*/
|
||||
export const selectActiveUsers = createSelector(
|
||||
[selectCursors],
|
||||
(cursors) => {
|
||||
const now = Date.now();
|
||||
const STALE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
|
||||
|
||||
// Group cursors by userId and keep the most recent one for each user
|
||||
const userMap = new Map<string, { userId: string; userName: string; lastUpdated: number }>();
|
||||
|
||||
for (const cursor of Object.values(cursors)) {
|
||||
// Filter out stale cursors
|
||||
if (now - cursor.lastUpdated > STALE_THRESHOLD_MS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep the most recent cursor for each user
|
||||
const existing = userMap.get(cursor.userId);
|
||||
if (!existing || cursor.lastUpdated > existing.lastUpdated) {
|
||||
userMap.set(cursor.userId, {
|
||||
userId: cursor.userId,
|
||||
userName: cursor.userName,
|
||||
lastUpdated: cursor.lastUpdated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to UserView format for UsersIndicator component
|
||||
return Array.from(userMap.values())
|
||||
.map((user) => ({
|
||||
user: {
|
||||
name: user.userName,
|
||||
},
|
||||
lastActiveAt: new Date(user.lastUpdated).toISOString(),
|
||||
}))
|
||||
.sort((a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime());
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select the entire legacy-compatible state
|
||||
* Useful for gradual migration
|
||||
|
||||
Reference in New Issue
Block a user