From a92352c20c5451bc08ba2cff38f6e647f8cf5b85 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Thu, 12 Mar 2026 16:09:13 -0700 Subject: [PATCH 1/5] Limit password filter to reset events --- frontend/src/components/EventList/eventTypes.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/EventList/eventTypes.tsx b/frontend/src/components/EventList/eventTypes.tsx index e558ede25..974cc6aad 100644 --- a/frontend/src/components/EventList/eventTypes.tsx +++ b/frontend/src/components/EventList/eventTypes.tsx @@ -21,9 +21,8 @@ export const eventFilterOptions: EventFilterOption[] = [ }, { key: 'password-activity', - label: 'Password Activity', - types: ['AUTH_PASSWORD_CHANGE', 'AUTH_PASSWORD_RESET'], - iconTypes: ['AUTH_PASSWORD_CHANGE'], + label: 'Password Reset', + types: ['AUTH_PASSWORD_RESET'], }, { key: 'phone-change', label: 'Phone Change', types: ['AUTH_PHONE_CHANGE'] }, { From 78d902be05e1fd750c416d004b123bb6d0ffa261 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Thu, 12 Mar 2026 17:39:18 -0700 Subject: [PATCH 2/5] fix(frontend): hide connection copy actions until ready --- frontend/src/components/ConnectAttribute.tsx | 22 ++++--- frontend/src/components/ConnectionDetails.tsx | 66 ++++++++++--------- frontend/src/helpers/connectionHelper.ts | 4 ++ 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/ConnectAttribute.tsx b/frontend/src/components/ConnectAttribute.tsx index d87240d29..daacb952e 100644 --- a/frontend/src/components/ConnectAttribute.tsx +++ b/frontend/src/components/ConnectAttribute.tsx @@ -4,9 +4,11 @@ import { CopyIconButton } from '../buttons/CopyIconButton' import { ComboButton } from '../buttons/ComboButton' import { LaunchButton } from '../buttons/LaunchButton' import { Box } from '@mui/material' +import { copyReady } from '../helpers/connectionHelper' export const ConnectAttribute = ({ device, service, connection }: IDataOptions) => { const app = useApplication(service, connection) + const canCopy = copyReady(connection) const buttons = connection && connection.online && @@ -23,15 +25,17 @@ export const ConnectAttribute = ({ device, service, connection }: IDataOptions) {buttons && ( <>   - + {canCopy && ( + + )} = ({ showTitle, show, app, conne let port = connection?.port const disabled = !(connection?.enabled || session) || !connection?.online + const canCopy = copyReady(connection) const endpoint = getEndpoint(name, port) const endpointName = (connection?.public || connection?.connectLink ? 'Public' : 'Local') + ' Endpoint' const secureReverseProxy = isSecureReverseProxy(app.string) @@ -76,6 +77,7 @@ export const ConnectionDetails: React.FC = ({ showTitle, show, app, conne variant="h3" className={css.h3} onClick={() => { + if (!canCopy) return buttonRef.current?.click() setCopied('Copied') }} @@ -193,43 +195,47 @@ export const ConnectionDetails: React.FC = ({ showTitle, show, app, conne Copy {hover === 'launch' ? '' : hover === 'copy' ? app.contextTitle : hover} - setHover('endpoint')} - onMouseLeave={() => setHover(undefined)} - onCopy={() => setCopied(undefined)} - /> - {connection?.host && ( + {canCopy && ( <> - {connection.port && ( + setHover('endpoint')} + onMouseLeave={() => setHover(undefined)} + onCopy={() => setCopied(undefined)} + /> + {connection?.host && ( <> + {connection.port && ( + <> + setHover('host')} + onMouseLeave={() => setHover(undefined)} + /> + setHover('port')} + onMouseLeave={() => setHover(undefined)} + /> + + )} setHover('host')} - onMouseLeave={() => setHover(undefined)} - /> - setHover('port')} + icon={app.copyIcon} + app={app} + value={app.sshConfigString} + onMouseEnter={() => setHover('copy')} onMouseLeave={() => setHover(undefined)} /> )} - setHover('copy')} - onMouseLeave={() => setHover(undefined)} - /> )} diff --git a/frontend/src/helpers/connectionHelper.ts b/frontend/src/helpers/connectionHelper.ts index 81f913d19..9c4ab601f 100644 --- a/frontend/src/helpers/connectionHelper.ts +++ b/frontend/src/helpers/connectionHelper.ts @@ -210,6 +210,10 @@ export function getEndpoint(name?: string, port?: number) { return name + (port ? ':' + port : '') } +export function copyReady(connection?: IConnection) { + return !!(connection?.connectLink || connection?.ready) +} + export function sanitizeUrl(name: string) { return name?.toLowerCase().replace(REGEX_CONNECTION_NAME, '-').replace(REGEX_CONNECTION_TRIM, '') } From b29b7585674ef06a7ec7862b208a42bb6798c895 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Thu, 12 Mar 2026 17:39:20 -0700 Subject: [PATCH 3/5] fix(frontend): resolve launch template defaults on reset --- common/src/applications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/applications.ts b/common/src/applications.ts index e23edc974..215a89d2a 100644 --- a/common/src/applications.ts +++ b/common/src/applications.ts @@ -152,7 +152,7 @@ export class Application { } get defaultTemplate() { - return this.launchMethod.defaultTemplate // need resolved + return this.resolvedDefaultTemplate(this.launchType) } get template() { From 21056bb7671e701faee22bfbcbc380143dcf7f3a Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Thu, 12 Mar 2026 17:39:24 -0700 Subject: [PATCH 4/5] refactor(frontend): move log filter types into app globals --- frontend/src/types.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index de9615db2..cb2f040cf 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -244,6 +244,16 @@ declare global { children?: React.ReactNode } + type LogsFilterContext = 'root' | 'device' + + type LogsFilterPreference = { + eventTypes?: IEventType[] + } + + type LogsFiltersByAccount = { + [accountId: string]: Partial> + } + type ILayout = { insets: SafeAreaInsets['insets'] & { topPx: string From 34492d67caded50aa99ae848d8fa381c917fbfc5 Mon Sep 17 00:00:00 2001 From: Jamie Ruderman Date: Thu, 12 Mar 2026 17:51:26 -0700 Subject: [PATCH 5/5] feat(frontend): persist log event filters by account --- .../src/components/EventList/EventHeader.tsx | 7 +++++- .../src/components/EventList/EventIcon.tsx | 1 - .../src/components/EventList/EventMessage.tsx | 7 ------ frontend/src/models/ui.ts | 23 +++++++++++++++++++ types.d.ts | 1 - 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/EventList/EventHeader.tsx b/frontend/src/components/EventList/EventHeader.tsx index 806262b42..6d4e08fce 100644 --- a/frontend/src/components/EventList/EventHeader.tsx +++ b/frontend/src/components/EventList/EventHeader.tsx @@ -24,19 +24,23 @@ const hasDateChanged = (lhs?: Date, rhs?: Date) => lhs?.getTime() !== rhs?.getTi export const EventHeader: React.FC<{ device?: IDevice }> = ({ device }) => { const dispatch = useDispatch() const { fetch, set } = dispatch.logs + const { setLogsFilter } = dispatch.ui const logLimit = useSelector((state: State) => selectLimit(state, undefined, 'log-limit')) const activeAccount = useSelector(selectActiveAccountId) const { events, minDate, selectedDate, eventTypes } = useSelector((state: State) => state.logs) + const logsFilters = useSelector((state: State) => state.ui.logsFilters) const allowedDays = Math.max(limitDays(logLimit?.value) || 0, 0) const minDateValue = useMemo(() => retentionStartDate(allowedDays, device), [allowedDays, device?.createdAt]) + const logsFilterContext: LogsFilterContext = device ? 'device' : 'root' + const savedEventTypes = activeAccount ? logsFilters[activeAccount]?.[logsFilterContext]?.eventTypes : undefined // Clear logs and fetch whenever device or account changes useEffect(() => { set({ after: undefined, - eventTypes: undefined, + eventTypes: savedEventTypes, events: { ...events, items: [] }, maxDate: undefined, selectedDate: undefined, @@ -77,6 +81,7 @@ export const EventHeader: React.FC<{ device?: IDevice }> = ({ device }) => { events: { ...events, items: [] }, planUpgrade: false, }) + setLogsFilter({ accountId: activeAccount, context: logsFilterContext, eventTypes: nextEventTypes }) fetch({ allowedDays, deviceId: device?.id }) } diff --git a/frontend/src/components/EventList/EventIcon.tsx b/frontend/src/components/EventList/EventIcon.tsx index c1c8230ef..59946f01e 100644 --- a/frontend/src/components/EventList/EventIcon.tsx +++ b/frontend/src/components/EventList/EventIcon.tsx @@ -21,7 +21,6 @@ export function EventIcon({ item, loggedInUser }: Props): JSX.Element { icon = 'arrow-right-to-bracket' color = 'grayDarker' break - case 'AUTH_PASSWORD_CHANGE': case 'AUTH_PASSWORD_RESET': icon = 'key-skeleton' color = 'grayDarker' diff --git a/frontend/src/components/EventList/EventMessage.tsx b/frontend/src/components/EventList/EventMessage.tsx index 78009e092..7ebc1badb 100644 --- a/frontend/src/components/EventList/EventMessage.tsx +++ b/frontend/src/components/EventList/EventMessage.tsx @@ -48,13 +48,6 @@ export function EventMessage({ ) break - case 'AUTH_PASSWORD_CHANGE': - message = ( - <> - {actorName} changed {actorAdjective} password - - ) - break case 'AUTH_PASSWORD_RESET': message = ( <> diff --git a/frontend/src/models/ui.ts b/frontend/src/models/ui.ts index a2b98ce80..0bd010d7d 100644 --- a/frontend/src/models/ui.ts +++ b/frontend/src/models/ui.ts @@ -33,6 +33,7 @@ const SAVED_ACROSS_LOGOUT = [ 'serviceTimeSeries', 'showDesktopNotice', 'mobileWelcome', + 'logsFilters', ] export type UIState = { @@ -109,6 +110,7 @@ export type UIState = { scriptForm?: IFileForm scriptRunForms: ILookup viewAsUser: { id: string; email: string } | null + logsFilters: LogsFiltersByAccount } export const defaultState: UIState = { @@ -207,6 +209,7 @@ export const defaultState: UIState = { scriptForm: undefined, scriptRunForms: {}, viewAsUser: null, + logsFilters: {}, } export default createModel()({ @@ -299,6 +302,26 @@ export default createModel()({ defaultSelection[id][key] = value dispatch.ui.set({ defaultSelection }) }, + async setLogsFilter( + { + accountId, + context, + eventTypes, + }: { + accountId?: string + context: LogsFilterContext + eventTypes?: IEventType[] + }, + state + ) { + const id = accountId || selectActiveAccountId(state) + if (!id) return + + const logsFilters = structuredClone(state.ui.logsFilters) + logsFilters[id] = logsFilters[id] || {} + logsFilters[id][context] = { eventTypes } + dispatch.ui.setPersistent({ logsFilters }) + }, async setPersistent(params: ILookup, state) { dispatch.ui.set(params) Object.keys(params).forEach(key => { diff --git a/types.d.ts b/types.d.ts index f93374a78..453bb2e48 100644 --- a/types.d.ts +++ b/types.d.ts @@ -837,7 +837,6 @@ declare global { type IEventType = | 'AUTH_LOGIN' | 'AUTH_LOGIN_ATTEMPT' - | 'AUTH_PASSWORD_CHANGE' | 'AUTH_PASSWORD_RESET' | 'AUTH_PHONE_CHANGE' | 'AUTH_MFA_ENABLED'