Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cac1cbd
feat: add device product page
evanrbowers Dec 11, 2025
83c1770
feat: add cloud sync for products
evanrbowers Dec 11, 2025
bfd5928
feat: match scripting and device page layout
evanrbowers Dec 11, 2025
5152e4e
feat: updated product list page
evanrbowers Dec 11, 2025
5d6c96a
fix: fix lock
evanrbowers Dec 12, 2025
008419d
feat: support multiple accounts for products
evanrbowers Dec 12, 2025
4f6ae7f
feat: remove attributes hidden and scope
evanrbowers Dec 12, 2025
4642f6d
feat: show icons for platform types and limit what platform types sho…
evanrbowers Dec 12, 2025
e23b7cd
feat: update for create product slide out instead of new page
evanrbowers Dec 12, 2025
7403454
feat: update product details to look like device details
evanrbowers Dec 12, 2025
77a7e09
feat: add back button
evanrbowers Dec 12, 2025
4941235
feat: add multi panel design
evanrbowers Dec 13, 2025
cb68cd5
fix: fix thresholds
evanrbowers Dec 13, 2025
e42d51a
feat: adjust sizing, add memory to orgs for selection/not selected, a…
evanrbowers Dec 16, 2025
c3353ff
feat: auto refresh and add refresh button
evanrbowers Dec 16, 2025
9ffab95
feat: DESK-1692 Add admin pages
evanrbowers Jan 6, 2026
ad9c8e9
Merge branch 'main' into admin-pages
evanrbowers Jan 7, 2026
3f7a481
incorporate pr changes and add updates for partner entity
evanrbowers Jan 14, 2026
9fcad8e
Merge branch 'main' into admin-pages
evanrbowers Jan 14, 2026
a63cb9b
feat: updates to admin pages for use and caching
evanrbowers Jan 22, 2026
5993725
feat: clean issues with partner entities
evanrbowers Jan 23, 2026
cf3eec3
fix: fixes for product creation and transfer
evanrbowers Jan 28, 2026
962ef97
fix: fix for registration command and spacing for lock layout
evanrbowers Jan 28, 2026
262ae8d
feat: remove link to registrations
evanrbowers Jan 28, 2026
ce99820
feat: add view as option in admin mode
evanrbowers Jan 28, 2026
0857d68
fix: fix date display for logs
evanrbowers Jan 28, 2026
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
16 changes: 11 additions & 5 deletions electron/src/ElectronApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,25 @@ export default class ElectronApp {
this.openWindow()
}

private handleNavigate = (action: 'BACK' | 'FORWARD' | 'STATUS') => {
private handleNavigate = (action: 'BACK' | 'FORWARD' | 'STATUS' | 'CLEAR') => {
if (!this.window) return
const canNavigate = {
canGoBack: this.window.webContents.navigationHistory.canGoBack,
canGoForward: this.window.webContents.navigationHistory.canGoForward,
}

switch (action) {
case 'BACK':
this.window.webContents.navigationHistory.goBack()
break
case 'FORWARD':
this.window.webContents.navigationHistory.goForward()
break
case 'CLEAR':
this.window.webContents.navigationHistory.clear()
break
}

// Get navigation state AFTER performing the action
const canNavigate = {
canGoBack: this.window.webContents.navigationHistory.canGoBack,
canGoForward: this.window.webContents.navigationHistory.canGoForward,
}
EventBus.emit(EVENTS.canNavigate, canNavigate)
}
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/buttons/RefreshButton/RefreshButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export const RefreshButton: React.FC<ButtonProps> = props => {
const logsPage = useRouteMatch(['/logs', '/devices/:deviceID/logs'])
const devicesPage = useRouteMatch('/devices')
const productsPage = useRouteMatch('/products')
const partnerStatsPage = useRouteMatch('/partner-stats')
const adminUsersPage = useRouteMatch('/admin/users')
const adminPartnersPage = useRouteMatch('/admin/partners')
const scriptingPage = useRouteMatch(['/script', '/scripts', '/runs'])
const scriptPage = useRouteMatch('/script')

Expand Down Expand Up @@ -85,6 +88,25 @@ export const RefreshButton: React.FC<ButtonProps> = props => {
} else if (productsPage) {
title = 'Refresh products'
methods.push(dispatch.products.fetch)

// partner stats pages
} else if (partnerStatsPage) {
title = 'Refresh partner stats'
methods.push(dispatch.partnerStats.fetch)

// admin users pages
} else if (adminUsersPage) {
title = 'Refresh users'
methods.push(async () => {
window.dispatchEvent(new CustomEvent('refreshAdminData'))
})

// admin partners pages
} else if (adminPartnersPage) {
title = 'Refresh partners'
methods.push(async () => {
window.dispatchEvent(new CustomEvent('refreshAdminData'))
})
}

const refresh = async () => {
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/components/AdminSidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react'
import { useHistory } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import { Icon } from './Icon'
import { State } from '../store'

export const AdminSidebarNav: React.FC = () => {
const history = useHistory()
const defaultSelection = useSelector((state: State) => state.ui.defaultSelection)
const currentPath = history.location.pathname

const handleNavClick = (baseRoute: string) => {
const adminSelection = defaultSelection['admin']
const savedRoute = adminSelection?.[baseRoute]
history.push(savedRoute || baseRoute)
}

return (
<List
sx={{
position: 'static',
'& .MuiListItemIcon-root': {
color: 'grayDark.main'
},
'& .MuiListItemText-primary': {
color: 'grayDarkest.main'
},
'& .MuiListItemButton-root:hover .MuiListItemText-primary': {
color: 'black.main'
},
'& .Mui-selected, & .Mui-selected:hover': {
backgroundColor: 'primaryLighter.main',
'& .MuiListItemIcon-root': {
color: 'grayDarker.main'
},
'& .MuiListItemText-primary': {
color: 'black.main',
fontWeight: 500
},
},
}}
>
<ListItemButton
dense
selected={currentPath.includes('/admin/users')}
onClick={() => handleNavClick('/admin/users')}
>
<ListItemIcon>
<Icon name="users" size="md" />
</ListItemIcon>
<ListItemText primary="Users" />
</ListItemButton>

<ListItemButton
dense
selected={currentPath.includes('/admin/partners')}
onClick={() => handleNavClick('/admin/partners')}
>
<ListItemIcon>
<Icon name="handshake" size="md" />
</ListItemIcon>
<ListItemText primary="Partners" />
</ListItemButton>
</List>
)
}

45 changes: 44 additions & 1 deletion frontend/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import browser from '../services/browser'
import useSafeArea from '../hooks/useSafeArea'
import useCapacitor from '../hooks/useCapacitor'
import { persistor } from '../store'
import { useLocation } from 'react-router-dom'
import { useLocation, useHistory } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import { selectResellerRef } from '../selectors/organizations'
import { useSelector, useDispatch } from 'react-redux'
Expand All @@ -27,17 +27,20 @@ import { Sidebar } from './Sidebar'
import { Router } from '../routers/Router'
import { Page } from '../pages/Page'
import { Logo } from '@common/brand/Logo'
import { ViewAsBanner } from './ViewAsBanner'

export const App: React.FC = () => {
const { insets } = useSafeArea()
const location = useLocation()
const history = useHistory()
const hideSplashScreen = useCapacitor()
const authInitialized = useSelector((state: State) => state.auth.initialized)
const installed = useSelector((state: State) => state.binaries.installed)
const signedOut = useSelector((state: State) => !state.auth.initialized || !state.auth.authenticated)
const waitMessage = useSelector((state: State) => state.ui.waitMessage)
const showOrgs = useSelector((state: State) => !!state.accounts.membership.length)
const reseller = useSelector(selectResellerRef)
const viewAsUser = useSelector((state: State) => state.ui.viewAsUser)
const dispatch = useDispatch<Dispatch>()
const hideSidebar = useMediaQuery(`(max-width:${HIDE_SIDEBAR_WIDTH}px)`)
const singlePanel = useMediaQuery(`(max-width:${HIDE_TWO_PANEL_WIDTH}px)`)
Expand All @@ -64,6 +67,44 @@ export const App: React.FC = () => {
dispatch.ui.set({ layout })
}, [insets, mobile, showOrgs, hideSidebar, showBottomMenu, singlePanel, sidePanelWidth])

// Handle viewAs URL parameter and restore from sessionStorage
useEffect(() => {
// First, try to restore from sessionStorage (survives refresh)
const savedViewAs = window.sessionStorage.getItem('viewAsUser')
if (savedViewAs) {
try {
const viewAsUser = JSON.parse(savedViewAs)
dispatch.ui.set({ viewAsUser })
console.log('Restored viewAs from sessionStorage:', viewAsUser)
} catch (e) {
console.error('Failed to parse viewAsUser from sessionStorage:', e)
}
}

// Then check URL parameter (for initial open)
const params = new URLSearchParams(location.search)
const viewAsParam = params.get('viewAs')

if (viewAsParam) {
const [userId, email] = viewAsParam.split(',')
if (userId && email) {
const viewAsUser = { id: userId, email: decodeURIComponent(email) }
dispatch.ui.set({ viewAsUser })
// Save to sessionStorage for persistence across refreshes
window.sessionStorage.setItem('viewAsUser', JSON.stringify(viewAsUser))
console.log('Set viewAs from URL:', viewAsUser)

// Remove the parameter from URL
params.delete('viewAs')
const newSearch = params.toString()
history.replace({
pathname: location.pathname,
search: newSearch ? `?${newSearch}` : '',
})
}
}
}, [location.search])

if (waitMessage)
return (
<Page>
Expand Down Expand Up @@ -98,6 +139,7 @@ export const App: React.FC = () => {

return (
<Page>
<ViewAsBanner />
<PersistGate persistor={persistor} loading={<LoadingMessage message="Restoring state..." />}>
<Box
sx={{
Expand All @@ -108,6 +150,7 @@ export const App: React.FC = () => {
flexDirection: 'row',
alignItems: 'start',
justifyContent: 'start',
marginTop: viewAsUser ? '33px' : 0,
}}
>
{hideSidebar ? <SidebarMenu /> : <Sidebar layout={layout} />}
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/components/AvatarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const AvatarMenu: React.FC = () => {
const backendAuthenticated = useSelector((state: State) => state.auth.backendAuthenticated)
const licenseIndicator = useSelector(selectLicenseIndicator)
const activeUser = useSelector(selectActiveUser)
const adminMode = useSelector((state: State) => state.ui.adminMode)
const userAdmin = useSelector((state: State) => state.auth.user?.admin || false)

const css = useStyles()
const handleOpen = () => {
Expand Down Expand Up @@ -99,6 +101,33 @@ export const AvatarMenu: React.FC = () => {
badge={licenseIndicator}
onClick={handleClose}
/>
{adminMode ? (
<ListItemLocation
dense
title="Return to App"
icon="arrow-left"
to="/devices"
onClick={async () => {
await dispatch.ui.set({ adminMode: false })
handleClose()
history.push('/devices')
}}
/>
) : (
userAdmin && (
<ListItemLocation
dense
title="Admin"
icon="user-shield"
to="/admin/users"
onClick={async () => {
await dispatch.ui.set({ adminMode: true })
handleClose()
history.push('/admin/users')
}}
/>
)
)}
<ListItemLink
title="Support"
icon="life-ring"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/EventList/EventItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function EventItem({ item, device, user }: { item: IEvent; device?: IDevi
if (item.type === 'DEVICE_REFRESH') return null
return (
<ListItem>
<span>{new Date(item.timestamp).toLocaleDateString(navigator.language, options)}</span>
<span>{new Date(item.timestamp).toLocaleString(navigator.language, options)}</span>
<ListItemIcon>
<EventIcon item={item} loggedInUser={user} />
</ListItemIcon>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export const Header: React.FC = () => {

const manager = permissions.includes('MANAGE')
const menu = location.pathname.match(REGEX_FIRST_PATH)?.[0]
const isRootMenu = menu === location.pathname

// Admin pages have two-level roots: /admin/users and /admin/partners (without IDs)
const adminRootPages = ['/admin/users', '/admin/partners', '/partner-stats']
const isAdminRootPage = adminRootPages.includes(location.pathname)
const isRootMenu = menu === location.pathname || isAdminRootPage

return (
<>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/OrganizationSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const OrganizationSelect: React.FC = () => {
const history = useHistory()
const location = useLocation()
const mobile = useMediaQuery(`(max-width:${MOBILE_WIDTH}px)`)
const { accounts, devices, files, tags, networks, logs, products } = useDispatch<Dispatch>()
const { accounts, devices, files, tags, networks, logs, products, partnerStats } = useDispatch<Dispatch>()

let activeOrg = useSelector(selectOrganization)
const defaultSelection = useSelector((state: State) => state.ui.defaultSelection)
Expand Down Expand Up @@ -58,7 +58,8 @@ export const OrganizationSelect: React.FC = () => {
files.fetchIfEmpty()
tags.fetchIfEmpty()
products.fetchIfEmpty()
if (!mobile && ['/devices', '/networks', '/connections', '/products'].includes(menu)) {
partnerStats.fetchIfEmpty()
if (!mobile && ['/devices', '/networks', '/connections', '/products', '/partner-stats'].includes(menu)) {
history.push(defaultSelection[id]?.[menu] || menu)
}
}
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@ import { OrganizationSidebar } from './OrganizationSidebar'
import { RemoteManagement } from './RemoteManagement'
import { RegisterMenu } from './RegisterMenu'
import { SidebarNav } from './SidebarNav'
import { AdminSidebarNav } from './AdminSidebarNav'
import { AvatarMenu } from './AvatarMenu'
import { spacing } from '../styling'
import { Body } from './Body'
import { useSelector } from 'react-redux'
import { State } from '../store'

export const Sidebar: React.FC<{ layout: ILayout }> = ({ layout }) => {
const addSpace = browser.isMac && browser.isElectron && !layout.showOrgs
const adminMode = useSelector((state: State) => state.ui.adminMode)
const css = useStyles({ insets: layout.insets, addSpace })

return (
<OrganizationSidebar insets={layout.insets} hide={!layout.showOrgs}>
<OrganizationSidebar insets={layout.insets} hide={!layout.showOrgs || adminMode}>
<Body className={css.sidebar} scrollbarBackground="grayLighter">
<section className={css.header}>
<AvatarMenu />
<RegisterMenu buttonSize={38} sidebar type="solid" />
{!adminMode && <RegisterMenu buttonSize={38} sidebar type="solid" />}
</section>
<SidebarNav />
<RemoteManagement />
{adminMode ? <AdminSidebarNav /> : <SidebarNav />}
{!adminMode && <RemoteManagement />}
</Body>
</OrganizationSidebar>
)
Expand Down
Loading