Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c900e73
feat: add method to get user's groups
allison-truhlar Sep 16, 2025
0b864f7
feat: add filtering by group to FileSharePathsHandler
allison-truhlar Oct 3, 2025
aac00a5
Merge branch 'main' into add-user-groups-to-api
allison-truhlar Oct 3, 2025
558b788
fix: return fsp is group is local
allison-truhlar Oct 3, 2025
f98a712
fix: update old variable name to new one
allison-truhlar Oct 3, 2025
bfeeb2c
feat: add method to get user's groups
allison-truhlar Sep 16, 2025
7e4984b
feat: add filtering by group to FileSharePathsHandler
allison-truhlar Oct 3, 2025
849b647
fix: return fsp is group is local
allison-truhlar Oct 3, 2025
087266c
fix: update old variable name to new one
allison-truhlar Oct 3, 2025
48f7b7f
refactor: fetch all preferences with one useEffect and network request
allison-truhlar Oct 3, 2025
62bb118
fix: revert get handler for file-share-paths back to original
allison-truhlar Oct 6, 2025
e07b392
feat: return groups in the profile get handler
allison-truhlar Oct 6, 2025
17e0e6f
feat: add isFilteredByGroups and toggle func to Preferences context
allison-truhlar Oct 6, 2025
58faef0
feat: add toggle for isFilteredByGroups to UI
allison-truhlar Oct 6, 2025
a84a200
refactor: add groups to profile data type
allison-truhlar Oct 6, 2025
bb85beb
refactor: add filtering by groups to hook; rename to reflect updated …
allison-truhlar Oct 6, 2025
e15c341
feat: add filtering message and link to prefs page to Zones browser
allison-truhlar Oct 6, 2025
81154ae
Merge branch 'filter-by-group-preference' into add-user-groups-to-api
allison-truhlar Oct 6, 2025
6ccd1c6
chore: linter changes
allison-truhlar Oct 6, 2025
b261805
Merge branch 'main' into add-user-groups-to-api
allison-truhlar Oct 6, 2025
7504eed
chore: linter
allison-truhlar Oct 6, 2025
6b641c4
fix: default to no filtering for local zones; make zones selector mor…
allison-truhlar Oct 6, 2025
a2ce63e
fix: try to fix docs link error in GH action
allison-truhlar Oct 6, 2025
283fdb6
fix: add add'tl context provider to test setup to fix failures
allison-truhlar Oct 6, 2025
a51abfb
fix: links for gh build action
allison-truhlar Oct 6, 2025
768faee
fix: toHaveTitle not working on page fixture
allison-truhlar Oct 6, 2025
23cf217
docs: update ui-test readme
allison-truhlar Oct 6, 2025
1b8874c
fix: update fileglancer-user-docs links to fileglancer-docs
allison-truhlar Oct 6, 2025
96591e2
fix: revert file share paths handler back to no group filtering
allison-truhlar Oct 7, 2025
45b87c8
Merge branch 'main' into add-user-groups-to-api
allison-truhlar Oct 7, 2025
9df8346
Merge branch 'main' into add-user-groups-to-api
allison-truhlar Oct 8, 2025
3f1b75a
update wording
krokicki Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ The current code base is geared towards a Janelia deployment, but we are working

- [fileglancer-central](https://github.com/JaneliaSciComp/fileglancer-central) - Central server managing access to a shared database and other resources
- [fileglancer-hub](https://github.com/JaneliaSciComp/fileglancer-hub) - Deployment of Fileglancer into JupyterHub
- [fileglancer-user-docs](https://github.com/JaneliaSciComp/fileglancer-docs) - User guide
- [fileglancer-docs](https://github.com/JaneliaSciComp/fileglancer-docs) - User guide
11 changes: 9 additions & 2 deletions docs/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,16 @@ pixi run test-frontend

This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
More information are provided within the [ui-tests](../ui-tests/README.md) README.

To execute the UI integration test, run:
To execute the UI integration tests:

Install test dependencies (needed only once):

```bash
pixi run npm --prefix ui-tests npx playwright install
```

Then run the tests with:

```bash
pixi run ui-test
Expand Down
41 changes: 38 additions & 3 deletions fileglancer/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
import requests
import re
import grp
import pwd
from datetime import datetime, timezone
from abc import ABC
from mimetypes import guess_type
Expand Down Expand Up @@ -52,10 +54,11 @@ def _get_mounted_filestore(fsp):
except FileNotFoundError:
return None
return filestore


class BaseHandler(APIHandler):
_home_file_share_path_cache = {}
_groups_cache = {}

def get_current_user(self):
"""
Expand Down Expand Up @@ -98,6 +101,36 @@ def get_home_file_share_path_name(self):
return None


def get_user_groups(self):
"""
Get the groups for the current user.

Returns:
list: List of group names the user belongs to.
"""
username = self.get_current_user()

if username in self._groups_cache:
return self._groups_cache[username]

try:
user_info = pwd.getpwnam(username)
user_groups = []
all_groups = grp.getgrall() # Get all groups on the system
for group in all_groups:
if username in group.gr_mem: # Check if user is a member of this group
user_groups.append(group.gr_name)
primary_group = grp.getgrgid(user_info.pw_gid).gr_name
if primary_group not in user_groups: # Add primary group if not already included
user_groups.append(primary_group)
self._groups_cache[username] = user_groups
return user_groups
except Exception as e:
self.log.error(f"Error getting groups for user {username}: {str(e)}")
self._groups_cache[username] = []
return []


class StreamingProxy(BaseHandler):
"""
API handler for proxying responses from the central server
Expand Down Expand Up @@ -132,6 +165,7 @@ class FileSharePathsHandler(BaseHandler):
def get(self):
self.log.info("GET /api/fileglancer/file-share-paths")
file_share_paths = get_fsp_manager(self.settings).get_file_share_paths()

self.set_header('Content-Type', 'application/json')
self.set_status(200)
# Convert Pydantic objects to dicts before JSON serialization
Expand All @@ -140,7 +174,6 @@ def get(self):
self.finish()



class FileShareHandler(BaseHandler, ABC):
"""
Abstract base handler for endpoints that use the Filestore class.
Expand Down Expand Up @@ -1028,11 +1061,13 @@ def get(self):
username = self.get_current_user()
home_fsp_name = self.get_home_file_share_path_name()
home_directory_name = os.path.basename(self.get_home_directory_path())
self.log.info(f"GET /api/fileglancer/profile username={username} home_fsp_name={home_fsp_name} home_directory_name={home_directory_name}")
groups = self.get_user_groups()
self.log.info(f"GET /api/fileglancer/profile username={username} home_fsp_name={home_fsp_name} home_directory_name={home_directory_name} groups={groups}")
response = {
"username": username,
"homeFileSharePathName": home_fsp_name,
"homeDirectoryName": home_directory_name,
"groups": groups,
}
try:
self.set_status(200)
Expand Down
33 changes: 18 additions & 15 deletions src/__tests__/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { OpenFavoritesProvider } from '@/contexts/OpenFavoritesContext';
import { TicketProvider } from '@/contexts/TicketsContext';
import { ProfileContextProvider } from '@/contexts/ProfileContext';
import { ExternalBucketProvider } from '@/contexts/ExternalBucketContext';
import { CentralServerHealthProvider } from '@/contexts/CentralServerHealthContext';
import ErrorFallback from '@/components/ErrorFallback';

interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
Expand All @@ -39,21 +40,23 @@ const FileBrowserTestingWrapper = ({
const Browse = ({ children }: { children: React.ReactNode }) => {
return (
<CookiesProvider>
<ZonesAndFspMapContextProvider>
<OpenFavoritesProvider>
<FileBrowserTestingWrapper>
<PreferencesProvider>
<ExternalBucketProvider>
<ProxiedPathProvider>
<ProfileContextProvider>
<TicketProvider>{children}</TicketProvider>
</ProfileContextProvider>
</ProxiedPathProvider>
</ExternalBucketProvider>
</PreferencesProvider>
</FileBrowserTestingWrapper>
</OpenFavoritesProvider>
</ZonesAndFspMapContextProvider>
<CentralServerHealthProvider>
<ZonesAndFspMapContextProvider>
<OpenFavoritesProvider>
<FileBrowserTestingWrapper>
<PreferencesProvider>
<ExternalBucketProvider>
<ProxiedPathProvider>
<ProfileContextProvider>
<TicketProvider>{children}</TicketProvider>
</ProfileContextProvider>
</ProxiedPathProvider>
</ExternalBucketProvider>
</PreferencesProvider>
</FileBrowserTestingWrapper>
</OpenFavoritesProvider>
</ZonesAndFspMapContextProvider>
</CentralServerHealthProvider>
</CookiesProvider>
);
};
Expand Down
32 changes: 31 additions & 1 deletion src/components/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export default function Preferences() {
pathPreference,
handlePathPreferenceSubmit,
hideDotFiles,
isFilteredByGroups,
toggleHideDotFiles,
disableNeuroglancerStateGeneration,
toggleDisableNeuroglancerStateGeneration,
disableHeuristicalLayerTypeDetection,
toggleDisableHeuristicalLayerTypeDetection
toggleDisableHeuristicalLayerTypeDetection,
toggleFilterByGroups
} = usePreferencesContext();

return (
Expand Down Expand Up @@ -124,6 +126,34 @@ export default function Preferences() {
<Typography className="font-semibold">Options:</Typography>
</Card.Header>
<Card.Body className="flex flex-col gap-4 pb-4">
<div className="flex items-center gap-2">
<input
checked={isFilteredByGroups}
className="icon-small checked:accent-secondary-light"
id="is_filtered_by_groups"
onChange={async () => {
const result = await toggleFilterByGroups();
if (result.success) {
toast.success(
!isFilteredByGroups
? 'Only Zones for groups you have membership in are now visible'
: 'All Zones are now visible'
);
} else {
toast.error(result.error);
}
}}
type="checkbox"
/>
<Typography
as="label"
className="text-foreground"
htmlFor="is_filtered_by_groups"
>
Display Zones for your groups only
</Typography>
</div>

<div className="flex items-center gap-2">
<input
checked={hideDotFiles}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/Sidebar/FavoritesBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ export default function FavoritesBrowser({
displayFolders.length === 0 ? (
<div className="px-4 py-6 text-center">
<Typography className="text-sm text-gray-500">
No favorites match your filter.
No favorites match your filter '{searchQuery}'
</Typography>
<Typography className="text-xs text-gray-400 mt-1">
Try broadening your search to see more results.
Try broadening your search to see more results
</Typography>
</div>
) : (
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HiOutlineFunnel, HiXMark } from 'react-icons/hi2';

import FavoritesBrowser from './FavoritesBrowser';
import ZonesBrowser from './ZonesBrowser';
import useSearchFilter from '@/hooks/useSearchFilter';
import useFilteredZonesAndFavorites from '@/hooks/useFilteredZonesAndFavorites';

export default function Sidebar() {
const {
Expand All @@ -15,7 +15,8 @@ export default function Sidebar() {
filteredZoneFavorites,
filteredFileSharePathFavorites,
filteredFolderFavorites
} = useSearchFilter();
} = useFilteredZonesAndFavorites();

return (
<Card className="min-w-full h-full overflow-hidden rounded-none bg-surface shadow-lg flex flex-col pl-3">
<div className="my-3 short:my-1 relative">
Expand Down
37 changes: 35 additions & 2 deletions src/components/ui/Sidebar/ZonesBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { HiSquares2X2 } from 'react-icons/hi2';

import { ZonesAndFileSharePathsMap } from '@/shared.types';
import { useZoneAndFspMapContext } from '@/contexts/ZonesAndFspMapContext';
import { usePreferencesContext } from '@/contexts/PreferencesContext';
import useOpenZones from '@/hooks/useOpenZones';
import Zone from './Zone';
import { SidebarItemSkeleton } from '@/components/ui/widgets/Loaders';
import { Link } from 'react-router';

export default function ZonesBrowser({
searchQuery,
Expand All @@ -17,6 +19,7 @@ export default function ZonesBrowser({
}) {
const { zonesAndFileSharePathsMap, areZoneDataLoading } =
useZoneAndFspMapContext();
const { isFilteredByGroups } = usePreferencesContext();
const { openZones, toggleOpenZones } = useOpenZones();

const displayZones: ZonesAndFileSharePathsMap =
Expand Down Expand Up @@ -58,10 +61,10 @@ export default function ZonesBrowser({
Object.keys(displayZones).length === 0 ? (
<div className="px-4 py-6 text-center">
<Typography className="text-sm text-gray-500">
No zones match your filter.
No zones match your filter '{searchQuery}'
</Typography>
<Typography className="text-xs text-gray-400 mt-1">
Try broadening your search to see more results.
Try broadening your search to see more results
</Typography>
</div>
) : (
Expand All @@ -78,6 +81,36 @@ export default function ZonesBrowser({
}
})
)}

<div className="px-4 py-6 text-center">
{isFilteredByGroups ? (
<>
<Typography className="text-sm text-gray-500">
Viewing Zones for your groups only
</Typography>
<Typography className="text-xs text-gray-400 mt-1">
Modify your{' '}
<Link className="text-primary underline" to="/preferences">
preferences
</Link>{' '}
to see all Zones
</Typography>
</>
) : (
<>
<Typography className="text-sm text-gray-500">
Viewing all Zones
</Typography>
<Typography className="text-xs text-gray-400 mt-1">
Modify your{' '}
<Link className="text-primary underline" to="/preferences">
preferences
</Link>{' '}
to see Zones for your groups only
</Typography>
</>
)}
</div>
</List>
)}
</Collapse>
Expand Down
Loading