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
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
timeout-minutes: 15
timeout-minutes: 25
run: |
npm ci --legacy-peer-deps
npm run build --if-present
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azure-iot-explorer",
"version": "0.15.18",
"version": "0.15.19",
"description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.",
"main": "host/electron.js",
"build": {
Expand Down Expand Up @@ -61,7 +61,7 @@
"start:dev": "concurrently \"npm run start:web:dev\" \"npm run start:electron:dev\"",
"start:web:dev": "npm run localization && npm run webpack:compile && webpack-dev-server --config webpack.dev.js --mode development --hot --port 3000 --host 127.0.0.1",
"start:electron:dev": "wait-on http://localhost:3000 && npm run electron:compile && npm run preload:compile && cross-env NODE_ENV=development electron .",
"test": "npm run localization && jest --coverage",
"test": "npm run localization && node --max-old-space-size=4096 ./node_modules/jest/bin/jest.js --coverage",
"test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand -i --watch",
"test:e2e": "npm run test:e2e:typecheck && npm run build && playwright test",
"test:e2e:cleanup": "tsx e2e/scripts/cleanupDevices.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/app/connectionStrings/components/commandBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useAuthenticationStateContext } from '../../authentication/context/auth

interface ConnectionStringCommandBarProps {
onAddConnectionStringClick: () => void;
addButtonRef?: React.Ref<HTMLButtonElement>;
}

export const ConnectionStringCommandBar: React.FC<ConnectionStringCommandBarProps> = props => {
Expand All @@ -25,6 +26,7 @@ export const ConnectionStringCommandBar: React.FC<ConnectionStringCommandBarProp
items={[
{
ariaLabel: t(ResourceKeys.connectionStrings.addConnectionCommand.ariaLabel),
buttonRef: props.addButtonRef,
disabled: state.payload.length >= CONNECTION_STRING_LIST_MAX_LENGTH,
icon: <AddRegular />,
key: 'add',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ describe('ConnectionString', () => {
const onEdit = jest.fn();
render(<MemoryRouter><ConnectionString {...defaultProps} onEditConnectionString={onEdit}/></MemoryRouter>);

fireEvent.click(screen.getByLabelText('connectionStrings.editConnectionCommand.ariaLabel'));
expect(onEdit).toHaveBeenCalledWith(testConnectionString);
const editButton = screen.getByLabelText('connectionStrings.editConnectionCommand.ariaLabel');
fireEvent.click(editButton);
expect(onEdit).toHaveBeenCalledWith(testConnectionString, editButton);
});

it('renders visit button that calls onSelectConnectionString', () => {
Expand Down
7 changes: 4 additions & 3 deletions src/app/connectionStrings/components/connectionString.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import './connectionString.scss';

export interface ConnectionStringProps {
connectionStringWithExpiry: ConnectionStringWithExpiry;
onEditConnectionString(connectionString: string): void;
onEditConnectionString(connectionString: string, invoker?: HTMLElement): void;
onDeleteConnectionString(connectionString: string): void;
onSelectConnectionString(connectionString: string): void;
}
Expand All @@ -34,8 +34,9 @@ export const ConnectionString: React.FC<ConnectionStringProps> = (props: Connect
const [ confirmingDelete, setConfirmingDelete ] = React.useState<boolean>(false);
const { t } = useTranslation();

const onEditConnectionStringClick = () => {
onEditConnectionString(connectionString);
const onEditConnectionStringClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Pass the button along so the edit drawer can return focus to it on close.
onEditConnectionString(connectionString, event.currentTarget);
};

const onDeleteConnectionStringClick = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { ConnectionStringsView } from './connectionStringsView';
import * as connectionStringContext from '../context/connectionStringStateContext';
Expand Down Expand Up @@ -73,4 +73,42 @@ describe('ConnectionStringsView', () => {
// Should NOT show empty state
expect(screen.queryByText('connectionStrings.empty.header')).toBeNull();
});

it('returns focus to the add button when the add drawer is dismissed', () => {
render(<MemoryRouter><ConnectionStringsView/></MemoryRouter>);

const addButton = screen.getByLabelText('connectionStrings.addConnectionCommand.ariaLabel');
addButton.focus();
fireEvent.click(addButton);

expect(screen.getByText('connectionStrings.editConnection.title.add')).toBeInTheDocument();

fireEvent.click(screen.getAllByLabelText('connectionStrings.editConnection.cancel.ariaLabel.add')[0]);

expect(document.activeElement).toBe(addButton);
});

it('returns focus to the edit button when the edit drawer is dismissed', () => {
(connectionStringContext.useConnectionStringContext as jest.Mock).mockReturnValue([
{
payload: [
{ connectionString: 'HostName=hub1.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key1', expiration: new Date(Date.now() + 365 * 86400000).toISOString() }
],
synchronizationStatus: 'fetched'
},
{ setConnectionStrings: jest.fn(), upsertConnectionString: jest.fn(), deleteConnectionString: jest.fn(), getConnectionStrings: mockGetConnectionStrings }
]);

render(<MemoryRouter><ConnectionStringsView/></MemoryRouter>);

const editButton = screen.getByLabelText('connectionStrings.editConnectionCommand.ariaLabel');
editButton.focus();
fireEvent.click(editButton);

expect(screen.getByText('connectionStrings.editConnection.title.edit')).toBeInTheDocument();

fireEvent.click(screen.getAllByLabelText('connectionStrings.editConnection.cancel.ariaLabel.edit')[0]);

expect(document.activeElement).toBe(editButton);
});
});
18 changes: 16 additions & 2 deletions src/app/connectionStrings/components/connectionStringsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const ConnectionStringsView: React.FC = () => {
useBreadcrumbEntry({name: t(ResourceKeys.breadcrumb.resources)});
const [ state, api ] = useConnectionStringContext();
const [ connectionStringUnderEdit, setConnectionStringUnderEdit ] = React.useState<string>(undefined);
const addButtonRef = React.useRef<HTMLButtonElement>(null);
Comment thread
raharri marked this conversation as resolved.
// The element that opened the drawer, so focus can be returned to it on close.
// The drawer is shared by the add button and every row's edit button, so this is
// assigned per invocation rather than being tied to a single control.
const drawerInvokerRef = React.useRef<HTMLElement>(null);

const connectionStringsWithExpiry = state.payload;
const synchronizationStatus = state.synchronizationStatus;
Expand Down Expand Up @@ -56,20 +61,29 @@ export const ConnectionStringsView: React.FC = () => {
};

const onAddConnectionStringClick = () => {
drawerInvokerRef.current = addButtonRef.current;
setConnectionStringUnderEdit('');
};

const onEditConnectionStringClick = (connectionString: string) => {
const onEditConnectionStringClick = (connectionString: string, invoker?: HTMLElement) => {
drawerInvokerRef.current = invoker ?? null;
setConnectionStringUnderEdit(connectionString);
};

const restoreFocusToDrawerInvoker = () => {
drawerInvokerRef.current?.focus();
drawerInvokerRef.current = null;
};

const onConnectionStringEditCommit = (connectionString: string) => {
onUpsertConnectionString(connectionString, connectionStringUnderEdit);
setConnectionStringUnderEdit(undefined);
restoreFocusToDrawerInvoker();
};

const onConnectionStringEditDismiss = () => {
setConnectionStringUnderEdit(undefined);
restoreFocusToDrawerInvoker();
};
Comment thread
raharri marked this conversation as resolved.

React.useEffect(() => {
Expand All @@ -95,7 +109,7 @@ export const ConnectionStringsView: React.FC = () => {

return (
<div>
<ConnectionStringCommandBar onAddConnectionStringClick={onAddConnectionStringClick}/>
<ConnectionStringCommandBar onAddConnectionStringClick={onAddConnectionStringClick} addButtonRef={addButtonRef}/>
<div className="connection-strings">
{connectionStringsWithExpiry.map(connectionStringWithExpiry =>
<ConnectionString
Expand Down
11 changes: 10 additions & 1 deletion src/app/constants/fluentV9Theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ const darkBrand: BrandVariants = {
};

export const v9ThemeLight: Theme = createLightTheme(lightBrand);
export const v9ThemeDark: Theme = createDarkTheme(darkBrand);

// Override the dark theme's primary button colors to maintain sufficient contrast with white text.
// Uses primary button colors from Azure portal dark mode
export const v9ThemeDark: Theme = {
...createDarkTheme(darkBrand),
colorBrandBackground: 'rgb(0, 120, 212)',
colorBrandBackgroundHover: 'rgb(16, 110, 190)',
colorBrandBackgroundPressed: darkBrand[20],
colorBrandBackgroundSelected: darkBrand[30],
Comment thread
raharri marked this conversation as resolved.
};

// High contrast dark (white on black) — use Teams HC theme as-is
export const v9ThemeHighContrastDark: Theme = teamsHighContrastTheme;
Expand Down
5 changes: 5 additions & 0 deletions src/app/css/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
}
.settings button {
color: themed('headerButtonColor');
// Fluent v9 paints its focus ring as an ::after border coloured by
// --colorStrokeFocus2 and forces outline-style: none, so the token has
// to be overridden. The default is black, which is invisible against
// the dark header background.
--colorStrokeFocus2: #{themed('headerButtonColor')};
}
.settings button svg {
color: var(--colorBrandForeground1);
Expand Down
9 changes: 3 additions & 6 deletions src/app/css/_notification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,16 @@

.notification-toast-body {
word-break: break-word;
@include themify($themes) {
background-color: themed('notificationBackgroundColor');
}
}

.Toastify__toast-container {
margin-top:25px;
z-index: 10000000 !important;
}

.toast-notification {
@include themify($themes) {
background-color: themed('notificationBackgroundColor');
}
}

.notification-toast-progress-bar {
@include themify($themes) {
background: themed('notificationProgressBackground') !important;
Expand Down
14 changes: 3 additions & 11 deletions src/app/css/_themes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,7 @@
disabledText: $dark-gray,
inputComponentBackgroundColor: #ffffff,
pivotOnHoverBackground: $textfield-disabled-background,
menuLinkBackground_Active: rgb(234, 234, 234),
menuLinkBackground_Hover: rgb(250, 250, 250),
menuLinkBorder_Active: rgb(0, 116, 204),
menuLinkColor_Hover: $link-blue,
menuLinkColor: rgb(51,51,51)
menuLinkBackground_Hover: rgb(250, 250, 250)
),
dark: (
headerTitleColor: #cccccc,
Expand Down Expand Up @@ -85,7 +81,7 @@
notificationWarningColor: $warningText,
notificationInfoColor: $infoText,
notificationTextColor: #cccccc,
notificationBackgroundColor: $dark-gray,
notificationBackgroundColor: var(--colorNeutralBackground1, #292929),
notificationProgressBackground: $light-medium-gray,
maskedCopyableColor: #cccccc,
maskedCopyableReadonlyBackground: #363636,
Expand All @@ -105,11 +101,7 @@
disabledText: $light-gray,
inputComponentBackgroundColor: $dark-gray,
pivotOnHoverBackground: #363636,
menuLinkBackground_Active: rgb(37, 37, 37),
menuLinkBackground_Hover: rgb(234, 234, 234),
menuLinkBorder_Active: rgb(75, 166, 216),
menuLinkColor_Hover: $link-blue,
menuLinkColor: rgb(248,248,248)
menuLinkBackground_Hover: $dark-medium-gray
),
highContrastWhite: (
headerTitleColor: $contrast-white-text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { CloudToDeviceMessage } from './cloudToDeviceMessage';
import * as AsyncSagaReducer from '../../../shared/hooks/useAsyncSagaReducer';
Expand Down Expand Up @@ -49,23 +48,22 @@ describe('CloudToDeviceMessage', () => {
expect(screen.getByText('cloudToDeviceMessage.properties.addCustomProperty')).toBeInTheDocument();
});

it('keeps a system property selection after the property list rerenders', async () => {
const user = userEvent.setup();
it('keeps a system property selection after the property list rerenders', () => {
render(<MemoryRouter><CloudToDeviceMessage/></MemoryRouter>);

await user.click(screen.getByRole('button', {
fireEvent.click(screen.getByRole('button', {
name: 'cloudToDeviceMessage.properties.addSystemProperty'
}));
await user.click(screen.getByRole('menuitem', {
fireEvent.click(screen.getByRole('menuitem', {
name: 'cloudToDeviceMessage.properties.systemProperties.ack.displayName'
}));

const ackDropdown = screen.getByRole('combobox');
await user.click(ackDropdown);
await user.click(screen.getByRole('option', {
fireEvent.click(ackDropdown);
fireEvent.click(screen.getByRole('option', {
name: 'cloudToDeviceMessage.properties.systemProperties.ack.full'
}));
await user.click(screen.getByRole('button', {
fireEvent.click(screen.getByRole('button', {
name: 'cloudToDeviceMessage.properties.addCustomProperty'
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@

a:link, a:visited, a:active {
text-decoration: none;
@include themify($themes) {
color: themed('menuLinkColor');
}
}

// Only the background is themed here. Fluent v9's Tab sets `color` directly on
// its `.fui-Tab__content` span, so a `color` on the `<a>` root never inherits.
a:hover {
@include themify($themes) {
color: themed('menuLinkColor_Hover');
background-color: themed('menuLinkBackground_Hover');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { DeviceQueryClause, DeviceQueryClauseProps } from './deviceQueryClause';
import { ParameterType, OperationType } from '../../../api/models/deviceQuery';
import { ParameterType } from '../../../api/models/deviceQuery';

describe('DeviceQueryClause', () => {
const defaultProps: DeviceQueryClauseProps = {
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('DeviceQueryClause', () => {
'deviceLists.query.searchPills.clause.parameterType.ariaLabel'
).textContent).toContain('deviceLists.query.searchPills.clause.parameterType.items.status');
expect(screen.getByLabelText(
'deviceLists.query.searchPills.clause.value.placeholder'
'deviceLists.query.searchPills.clause.value.ariaLabel'
).textContent).toContain('deviceLists.query.searchPills.clause.value.deviceStatus.enabled');
});
});
8 changes: 6 additions & 2 deletions src/app/devices/deviceList/components/deviceQueryClause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,18 @@ export const DeviceQueryClause: React.FC<DeviceQueryClauseProps & DeviceQueryCla
};

const renderValueInput = () => {
const valueAriaLabel = t(
ResourceKeys.deviceLists.query.searchPills.clause.value.ariaLabel,
{ parameter: getParameterTypeText(parameterType) }
);
switch (parameterType) {
case ParameterType.edge:
return (
<Dropdown
className="clause-value"
onOptionSelect={onValueDropdownChange}
placeholder={t(ResourceKeys.deviceLists.query.searchPills.clause.value.placeholder)}
aria-label={t(ResourceKeys.deviceLists.query.searchPills.clause.value.placeholder)}
aria-label={valueAriaLabel}
selectedOptions={value ? [value] : []}
value={value ? getValueText(value) : ''}
>
Expand All @@ -158,7 +162,7 @@ export const DeviceQueryClause: React.FC<DeviceQueryClauseProps & DeviceQueryCla
className="clause-value"
onOptionSelect={onValueDropdownChange}
placeholder={t(ResourceKeys.deviceLists.query.searchPills.clause.value.placeholder)}
aria-label={t(ResourceKeys.deviceLists.query.searchPills.clause.value.placeholder)}
aria-label={valueAriaLabel}
selectedOptions={value ? [value] : []}
value={value ? getValueText(value) : ''}
>
Expand Down
Loading
Loading