Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,29 @@ export const CompactLocationsPage = () => {
}}
>
<WindowHeader variant="compact" />
<InstanceSwitcher />
<ScrollContainer>
<div className="main-content">
<InstanceSwitcher />
<div className="locations">
{displayedLocations.map((location) => {
const isOpen =
location.id === openLocation || displayedLocations.length === 1;
return (
<LocationCard
instance={instanceInfo}
disableOpen={displayedLocations.length <= 1}
location={location}
key={`${location.connection_type}-${location.id}`}
isOpen={isOpen}
onOpen={() => {
if (isOpen) {
useAppStore.setState({ expandedLocation: null });
} else {
useAppStore.setState({ expandedLocation: location.id });
}
}}
/>
);
})}
</div>
<div className="locations">
{displayedLocations.map((location) => {
const isOpen =
location.id === openLocation || displayedLocations.length === 1;
return (
<LocationCard
instance={instanceInfo}
disableOpen={displayedLocations.length <= 1}
location={location}
key={`${location.connection_type}-${location.id}`}
isOpen={isOpen}
onOpen={() => {
if (isOpen) {
useAppStore.setState({ expandedLocation: null });
} else {
useAppStore.setState({ expandedLocation: location.id });
}
}}
/>
);
})}
</div>
</ScrollContainer>
<div className="compact-footer">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { platform } from '@tauri-apps/plugin-os';
import clsx from 'clsx';
import { useMemo } from 'react';
import { Select } from '../../../../shared/components/Select/Select';
import type {
Expand All @@ -14,6 +16,8 @@ import {
import type { OverviewViewSelection } from '../../../../shared/rust-api/types';
import { isPresent } from '../../../../shared/utils/isPresent';

const isWindows = platform() === 'windows';

export const InstanceSwitcher = () => {
const { viewSelection: selectedInstance, setViewSelection } = useAppData();

Expand Down Expand Up @@ -76,12 +80,18 @@ export const InstanceSwitcher = () => {
if (totalOptions <= 1) return null;

return (
<Select
groups={groups}
value={selectedOption as never}
onChange={(option) => {
setViewSelection(option.value);
}}
/>
<div
className={clsx('instance-switcher', {
windows: isWindows,
})}
>
<Select
groups={groups}
value={selectedOption as never}
onChange={(option) => {
setViewSelection(option.value);
}}
/>
</div>
);
};
21 changes: 14 additions & 7 deletions new-ui/src/pages/compact/CompactLocationsPage/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@
flex-flow: column;
height: 100dvh;

> .compact-footer {
& > .compact-footer {
display: flex;
flex-flow: column;
}

> .window-header,
> .compact-footer {
& > .window-header,
& > .compact-footer {
flex: 0 0 auto;
}

.main-content {
display: flex;
flex-flow: column;
row-gap: var(--spacing-sm);
& > .instance-switcher {
flex: 0 0 auto;
margin-bottom: var(--spacing-sm);
box-sizing: border-box;

// lines the select up with the cards inside .scroll-container.windows
&.windows {
scrollbar-gutter: stable;
overflow-y: hidden;
padding-right: var(--scroll-container-gutter);
}
}

.locations {
Expand Down
19 changes: 14 additions & 5 deletions new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useNavigate } from '@tanstack/react-router';
import { useCallback } from 'react';
import { isPresent } from '../../../shared/utils/isPresent';
import {
ConfigureMfaTimeoutProvider,
useConfigureMfaSessionExpired,
Expand All @@ -18,16 +19,24 @@ export const ConfigureMfaPage = () => {

const ConfigureMfaContent = () => {
const navigate = useNavigate();
const authorized = useConfigureMfaStore((s) => s.authorized);
// a late authorization on the selection screen waits for the picks
const inWizard = useConfigureMfaStore(
(s) => s.authorized && isPresent(s.selectedMethods),
);
const handleSessionExpired = useConfigureMfaSessionExpired();

const leave = useCallback(() => {
navigate({ to: '/full/add' });
}, [navigate]);

return authorized ? (
<ConfigureMfaWizard onCancel={leave} onSessionExpired={handleSessionExpired} />
) : (
<ConfigureMfaVerify onCancel={leave} onSessionExpired={handleSessionExpired} />
return (
<>
{inWizard && (
<ConfigureMfaWizard onCancel={leave} onSessionExpired={handleSessionExpired} />
)}
{!inWizard && (
<ConfigureMfaVerify onCancel={leave} onSessionExpired={handleSessionExpired} />
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ const factorText = (method: MfaMethodValue): FactorText => {
description: `We'll send a temporary security code to your email. Enter the code to confirm it's you and continue.`,
};
case MfaMethod.Oidc:
return { title: '', description: '' };
return {
title: 'OpenID sign-in',
description: `Sign in with your identity provider in the browser to confirm it's you.`,
};
case MfaMethod.Biometric:
return { title: '', description: '' };
case MfaMethod.MobileApprove:
return { title: '', description: '' };
case MfaMethod.Fido2:
return { title: '', description: '' };
return {
title: 'Security key',
description: `Use a security key registered to your account, such as a YubiKey, to confirm it's you.`,
};
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MfaMethod } from '../../../../shared/rust-api/types';
import { ConfigureMfaStep } from '../types';
import { applyAuthorization, useConfigureMfaStore } from './useConfigureMfaStore';

vi.mock('@tauri-apps/plugin-log', () => ({ error: vi.fn() }));
vi.mock('../../../../shared/rust-api/api', () => ({ api: {} }));

const sessionId = 'session-1';
const authorizeResult = { deadline_timestamp: 1_900_000_000, recovery_codes: [] };

describe('applyAuthorization', () => {
beforeEach(() => {
useConfigureMfaStore.getState().reset();
useConfigureMfaStore.setState({
sessionId,
configuredMethods: [MfaMethod.Email],
verificationMethods: [MfaMethod.Email],
});
});

it('waits on the selection when the answer lands after Back', () => {
useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]);
useConfigureMfaStore.getState().backFromVerification();
applyAuthorization(sessionId, authorizeResult);

const state = useConfigureMfaStore.getState();
expect(state.authorized).toBe(true);
expect(state.selectedMethods).toBeNull();
expect(state.activeStep).toBe(ConfigureMfaStep.Configuration);
expect(state.deadline).not.toBeNull();
});

it('sets up the picks confirmed after a late answer', () => {
useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]);
useConfigureMfaStore.getState().backFromVerification();
applyAuthorization(sessionId, authorizeResult);
useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]);

const state = useConfigureMfaStore.getState();
expect(state.activeStep).toBe(ConfigureMfaStep.Fido2);
expect(state.deadline).not.toBeNull();
});

it('finishes when the pick confirmed after a late answer is empty', () => {
useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]);
useConfigureMfaStore.getState().backFromVerification();
applyAuthorization(sessionId, authorizeResult);
useConfigureMfaStore.getState().selectMethods([]);

const state = useConfigureMfaStore.getState();
expect(state.activeStep).toBe(ConfigureMfaStep.Finish);
expect(state.deadline).toBeNull();
});

it('keeps the deadline of an unauthorized session with an empty pick', () => {
useConfigureMfaStore.setState({ deadline: '2030-01-01T00:00:00.000Z' });
useConfigureMfaStore.getState().selectMethods([]);

expect(useConfigureMfaStore.getState().deadline).toBe('2030-01-01T00:00:00.000Z');
});

it('drops an answer for another session', () => {
useConfigureMfaStore.getState().selectMethods([MfaMethod.Fido2]);
applyAuthorization('session-2', authorizeResult);

expect(useConfigureMfaStore.getState().authorized).toBe(false);
});
});
Loading
Loading