Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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>
)
}

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 ? (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need a new boolean to track admin mode since we already have TestUI mode. We could rename testUI mode to admin mode and trigger that menu's visibility from the admin attribute on the user. And then just always show the admin menu items... maybe under the "more" menu? Having it integrated instead of separated? Maybe there is a reason to do it how you have it. This just seems simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd pref to keep it as its own pages and not nested. It also does not fit well into the multi account of the main page.

<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
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
31 changes: 16 additions & 15 deletions frontend/src/components/SidebarNav.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
import React, { useState } from 'react'
import React from 'react'
import browser from '../services/browser'
import { makeStyles } from '@mui/styles'
import { MOBILE_WIDTH } from '../constants'
import { selectLimitsLookup } from '../selectors/organizations'
import { selectDefaultSelectedPage } from '../selectors/ui'
import { selectActiveAccountId } from '../selectors/accounts'
import { useSelector, useDispatch } from 'react-redux'
import { State, Dispatch } from '../store'
import {
Box,
Badge,
List,
ListItemButton,
Divider,
Typography,
Tooltip,
Collapse,
Chip,
useMediaQuery,
} from '@mui/material'
import { ListItemLocation } from './ListItemLocation'
import { UpgradeBanner } from './UpgradeBanner'
import { ResellerLogo } from './ResellerLogo'
import { ListItemLink } from './ListItemLink'
import { ExpandIcon } from './ExpandIcon'
import { isRemoteUI } from '../helpers/uiHelper'
import { useCounts } from '../hooks/useCounts'
import { spacing } from '../styling'
import { getPartnerStatsModel } from '../models/partnerStats'

export const SidebarNav: React.FC = () => {
const [more, setMore] = useState<boolean>()
const counts = useCounts()
const reseller = useSelector((state: State) => state.user.reseller)
const defaultSelectedPage = useSelector(selectDefaultSelectedPage)
Expand All @@ -41,6 +38,10 @@ export const SidebarNav: React.FC = () => {
const css = useStyles({ active: counts.active, insets })
const pathname = path => (rootPaths ? path : defaultSelectedPage[path] || path)

// Check if user has admin access to any partner entities
const partnerStatsModel = useSelector((state: State) => getPartnerStatsModel(state))
const hasPartnerAdminAccess = partnerStatsModel.initialized && partnerStatsModel.all.length > 0

if (remoteUI)
return (
<List className={css.list}>
Expand Down Expand Up @@ -112,17 +113,17 @@ export const SidebarNav: React.FC = () => {
dense
/>
<ListItemLocation title="Organization" to="/organization" icon="industry-alt" dense />
{hasPartnerAdminAccess && (
<ListItemLocation
title="Partner Stats"
to="/partner-stats"
icon="chart-pie"
dense
onClick={() => dispatch.partnerStats.fetchIfEmpty()}
/>
)}
<ListItemLocation title="Products" to="/products" match="/products" icon="box" dense />
<ListItemLocation title="Logs" to="/logs" icon="rectangle-history" dense exactMatch />
<ListItemButton onClick={() => setMore(!more)} sx={{ marginTop: 2 }}>
<Typography variant="subtitle2" color="grayDark.main" marginLeft={1}>
More
<ExpandIcon open={more} color="grayDark" />
</Typography>
</ListItemButton>
<Collapse in={more}>
<ListItemLink title="Registrations" href="https://link.remote.it/app/registrations" icon="upload" dense />
</Collapse>
<Box className={css.footer}>
<UpgradeBanner />
<ResellerLogo reseller={reseller} marginLeft={4} size="small">
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/hooks/useContainerWidth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useRef, useState, useEffect } from 'react'

/**
* Hook to track the width of a container element using ResizeObserver
* @returns containerRef - ref to attach to the container element
* @returns containerWidth - current width of the container
*/
export function useContainerWidth() {
const containerRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState<number>(1000)

useEffect(() => {
const updateWidth = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth)
}
}

updateWidth()

const resizeObserver = new ResizeObserver(updateWidth)
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}

return () => resizeObserver.disconnect()
}, [])

return { containerRef, containerWidth }
}
Loading