зеркало из
1
0
Форкнуть 0
* Rkessler/resource view (#14)

Adds resource view.

* Rename and consolidate per pr.
This commit is contained in:
chieftn 2020-04-14 19:50:44 -07:00 коммит произвёл GitHub
Родитель 15357ce289
Коммит be1f8b90f9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
37 изменённых файлов: 1944 добавлений и 9 удалений

Просмотреть файл

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`azureResourcesView matches snapshot 1`] = `
<Component
history={[MockFunction]}
location={[MockFunction]}
match={
Object {
"params": Object {
"hostName": "hostName",
},
"url": "currentUrl",
}
}
/>
`;

Просмотреть файл

@ -0,0 +1,25 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow } from 'enzyme';
import { AzureResourcesView } from './azureResourcesView';
describe('azureResourcesView', () => {
it('matches snapshot', () => {
const routerprops = {
history: jest.fn() as any, // tslint:disable-line:no-any
location: jest.fn() as any, // tslint:disable-line:no-any
match: {
params: {
hostName: 'hostName'
},
url: 'currentUrl',
} as any // tslint:disable-line:no-any
};
const wrapper = shallow(<AzureResourcesView {...routerprops} />);
expect(wrapper).toMatchSnapshot();
});
});

Просмотреть файл

@ -0,0 +1,11 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { ConnectionStringsViewContainer } from '../../connectionStrings/components/connectionStringsView';
export const AzureResourcesView: React.FC<RouteComponentProps> = props => {
return <ConnectionStringsViewContainer {...props} />;
};

Просмотреть файл

@ -5,7 +5,8 @@
import {
addConnectionStringAction,
deleteConnectionStringAction,
setConnectionStringsAction
setConnectionStringsAction,
upsertConnectionStringAction
} from './actions';
describe('addConnectionStringAction', () => {
@ -34,3 +35,12 @@ describe('setConnectionStringAction', () => {
});
});
});
describe('upsertConnectionStringAction', () => {
it('returns CONNECTION_STRINGS/UPSERT action object', () => {
expect(upsertConnectionStringAction({ newConnectionString: 'new', connectionString: 'old'})).toEqual({
payload: { newConnectionString: 'new', connectionString: 'old'},
type: 'CONNECTION_STRINGS/UPSERT'
});
});
});

Просмотреть файл

@ -3,11 +3,17 @@
* Licensed under the MIT License
**********************************************************/
import actionCreatorFactory from 'typescript-fsa';
import { ADD, DELETE, SET } from '../constants/actionTypes';
import { ADD, DELETE, SET, UPSERT } from '../constants/actionTypes';
export const CONNECTION_STRINGS = 'CONNECTION_STRINGS';
export interface UpsertConnectionStringActionPayload {
newConnectionString: string;
connectionString?: string;
}
const actionCreator = actionCreatorFactory(CONNECTION_STRINGS);
export const addConnectionStringAction = actionCreator<string>(ADD);
export const deleteConnectionStringAction = actionCreator<string>(DELETE);
export const setConnectionStringsAction = actionCreator<string[]>(SET);
export const upsertConnectionStringAction = actionCreator<UpsertConnectionStringActionPayload>(UPSERT);

Просмотреть файл

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`connectionString matches snapshot 1`] = `
<div
className="connection-string"
>
<div
className="commands"
>
<div
className="name"
>
<StyledLinkBase
ariaLabel="connectionStrings.visitConnectionCommand.ariaLabel"
onClick={[Function]}
title="test"
>
test
</StyledLinkBase>
</div>
<div
className="actions"
>
<CustomizedIconButton
ariaLabel="connectionStrings.editConnectionCommand.ariaLabel"
iconProps={
Object {
"iconName": "EditSolid12",
}
}
onClick={[Function]}
title="connectionStrings.editConnectionCommand.label"
/>
<CustomizedIconButton
ariaLabel="connectionStrings.deleteConnectionCommand.ariaLabel"
iconProps={
Object {
"iconName": "Delete",
}
}
onClick={[Function]}
title="connectionStrings.deleteConnectionCommand.label"
/>
</div>
</div>
<div
className="properties"
>
<Component
connectionString="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
hostName="test.azure-devices-int.net"
sharedAccessKey="key"
sharedAccessKeyName="iothubowner"
/>
<Connect(MaskedCopyableTextField)
allowMask={false}
ariaLabel="connectionStrings.properties.connectionString.ariaLabel"
label="connectionStrings.properties.connectionString.label"
readOnly={true}
value="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
/>
</div>
<Component
connectionString="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
hidden={true}
onDeleteCancel={[Function]}
onDeleteConfirm={[Function]}
/>
</div>
`;

Просмотреть файл

@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionStringDelete matches snapshot hidden 1`] = `
<StyledWithResponsiveMode
dialogContentProps={
Object {
"title": "connectionStrings.deleteConnection.title",
}
}
hidden={true}
modalProps={
Object {
"isBlocking": true,
}
}
onDismiss={[MockFunction]}
>
<div
className="connection-string-delete"
>
<div>
connectionStrings.deleteConnection.body
</div>
<textarea
aria-label="connectionStrings.deleteConnection.input"
cols={40}
readOnly={true}
rows={8}
>
connectionString
</textarea>
</div>
<StyledDialogFooterBase>
<CustomizedPrimaryButton
ariaLabel="connectionStrings.deleteConnection.yes.ariaLabel"
onClick={[MockFunction]}
text="connectionStrings.deleteConnection.yes.label"
/>
<CustomizedDefaultButton
ariaLabel="connectionStrings.deleteConnection.no.ariaLabel"
onClick={[MockFunction]}
text="connectionStrings.deleteConnection.no.label"
/>
</StyledDialogFooterBase>
</StyledWithResponsiveMode>
`;
exports[`ConnectionStringDelete matches snapshot visible 1`] = `
<StyledWithResponsiveMode
dialogContentProps={
Object {
"title": "connectionStrings.deleteConnection.title",
}
}
hidden={false}
modalProps={
Object {
"isBlocking": true,
}
}
onDismiss={[MockFunction]}
>
<div
className="connection-string-delete"
>
<div>
connectionStrings.deleteConnection.body
</div>
<textarea
aria-label="connectionStrings.deleteConnection.input"
cols={40}
readOnly={true}
rows={8}
>
connectionString
</textarea>
</div>
<StyledDialogFooterBase>
<CustomizedPrimaryButton
ariaLabel="connectionStrings.deleteConnection.yes.ariaLabel"
onClick={[MockFunction]}
text="connectionStrings.deleteConnection.yes.label"
/>
<CustomizedDefaultButton
ariaLabel="connectionStrings.deleteConnection.no.ariaLabel"
onClick={[MockFunction]}
text="connectionStrings.deleteConnection.no.label"
/>
</StyledDialogFooterBase>
</StyledWithResponsiveMode>
`;

Просмотреть файл

@ -0,0 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionStringEdit matches snapshot in Add Scenario 1`] = `
<StyledPanelBase
closeButtonAriaLabel="connectionStrings.editConnection.cancel.ariaLabel.add"
isBlocking={true}
isFooterAtBottom={true}
isOpen={true}
onDismiss={[MockFunction]}
onRenderFooter={[Function]}
onRenderHeader={[Function]}
type={3}
>
<div
className="connection-string-edit-body"
>
<StyledTextFieldBase
ariaLabel="connectionStrings.editConnection.editField.ariaLabel"
label="connectionStrings.editConnection.editField.label"
multiline={true}
onChange={[Function]}
placeholder="connectionStrings.editConnection.editField.placeholder"
required={true}
rows={8}
value=""
/>
</div>
</StyledPanelBase>
`;
exports[`ConnectionStringEdit matches snapshot in Edit / invalid scenario 1`] = `
<StyledPanelBase
closeButtonAriaLabel="connectionStrings.editConnection.cancel.ariaLabel.edit"
isBlocking={true}
isFooterAtBottom={true}
isOpen={true}
onDismiss={[MockFunction]}
onRenderFooter={[Function]}
onRenderHeader={[Function]}
type={3}
>
<div
className="connection-string-edit-body"
>
<StyledTextFieldBase
ariaLabel="connectionStrings.editConnection.editField.ariaLabel"
label="connectionStrings.editConnection.editField.label"
multiline={true}
onChange={[Function]}
placeholder="connectionStrings.editConnection.editField.placeholder"
required={true}
rows={8}
value="connectionString"
/>
</div>
</StyledPanelBase>
`;
exports[`ConnectionStringEdit matches snapshot in Edit / valid scenario 1`] = `
<StyledPanelBase
closeButtonAriaLabel="connectionStrings.editConnection.cancel.ariaLabel.edit"
isBlocking={true}
isFooterAtBottom={true}
isOpen={true}
onDismiss={[MockFunction]}
onRenderFooter={[Function]}
onRenderHeader={[Function]}
type={3}
>
<div
className="connection-string-edit-body"
>
<StyledTextFieldBase
ariaLabel="connectionStrings.editConnection.editField.ariaLabel"
label="connectionStrings.editConnection.editField.label"
multiline={true}
onChange={[Function]}
placeholder="connectionStrings.editConnection.editField.placeholder"
required={true}
rows={8}
value="HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key"
/>
</div>
</StyledPanelBase>
`;
exports[`ConnectionStringEdit matches snapshot in Edit Scenario 1`] = `
<StyledPanelBase
closeButtonAriaLabel="connectionStrings.editConnection.cancel.ariaLabel.edit"
isBlocking={true}
isFooterAtBottom={true}
isOpen={true}
onDismiss={[MockFunction]}
onRenderFooter={[Function]}
onRenderHeader={[Function]}
type={3}
>
<div
className="connection-string-edit-body"
>
<StyledTextFieldBase
ariaLabel="connectionStrings.editConnection.editField.ariaLabel"
label="connectionStrings.editConnection.editField.label"
multiline={true}
onChange={[Function]}
placeholder="connectionStrings.editConnection.editField.placeholder"
required={true}
rows={8}
value="connectionString"
/>
</div>
</StyledPanelBase>
`;

Просмотреть файл

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionSTringProperties matches snapshot 1`] = `
<Fragment>
<Connect(MaskedCopyableTextField)
allowMask={false}
ariaLabel="connectionStrings.properties.hostName.ariaLabel"
label="connectionStrings.properties.hostName.label"
readOnly={true}
value="hostName"
/>
<Connect(MaskedCopyableTextField)
allowMask={false}
ariaLabel="connectionStrings.properties.sharedAccessPolicyName.ariaLabel"
label="connectionStrings.properties.sharedAccessPolicyName.label"
readOnly={true}
value="sharedAccessKeyName"
/>
<Connect(MaskedCopyableTextField)
allowMask={false}
ariaLabel="connectionStrings.properties.sharedAccessPolicyKey.ariaLabel"
label="connectionStrings.properties.sharedAccessPolicyKey.label"
readOnly={true}
value="sharedAccessKey"
/>
</Fragment>
`;

Просмотреть файл

@ -0,0 +1,111 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionStringsView matches snapshot when connection string count exceeds max 1`] = `
<div
className="view"
>
<div
className="view-command"
>
<StyledCommandBarBase
items={
Array [
Object {
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
"disabled": true,
"iconProps": Object {
"iconName": "Add",
},
"key": "add",
"onClick": [Function],
"text": "connectionStrings.addConnectionCommand.label",
},
]
}
/>
</div>
<div
className="view-content view-scroll-vertical"
>
<div
className="connection-strings"
/>
</div>
</div>
`;
exports[`ConnectionStringsView matches snapshot when connection strings present 1`] = `
<div
className="view"
>
<div
className="view-command"
>
<StyledCommandBarBase
items={
Array [
Object {
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
"disabled": false,
"iconProps": Object {
"iconName": "Add",
},
"key": "add",
"onClick": [Function],
"text": "connectionStrings.addConnectionCommand.label",
},
]
}
/>
</div>
<div
className="view-content view-scroll-vertical"
>
<div
className="connection-strings"
>
<Component
connectionString="connectionString1"
key="connectionString1"
onDeleteConnectionString={[MockFunction]}
onEditConnectionString={[Function]}
onSelectConnectionString={[MockFunction]}
/>
</div>
</div>
</div>
`;
exports[`ConnectionStringsView matches snapshot when no connection strings 1`] = `
<div
className="view"
>
<div
className="view-command"
>
<StyledCommandBarBase
items={
Array [
Object {
"ariaLabel": "connectionStrings.addConnectionCommand.ariaLabel",
"disabled": false,
"iconProps": Object {
"iconName": "Add",
},
"key": "add",
"onClick": [Function],
"text": "connectionStrings.addConnectionCommand.label",
},
]
}
/>
</div>
<div
className="view-content view-scroll-vertical"
>
<div
className="connection-strings"
/>
</div>
</div>
`;

Просмотреть файл

@ -0,0 +1,56 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import '../../css/themes';
@import '../../css/variables';
.connection-string {
margin-left: 20px;
margin-right: 20px;
margin-bottom: 20px;
margin-top: 20px;
width: 550px;
.commands {
display: flex;
justify-content: space-between
}
.actions {
margin-top: 5px;
margin-right: 5px;
justify-content: right;
}
.name {
@include themify($themes) {
border: 1px solid themed('borderColor')
}
@include themify($themes) {
border-bottom: 1px solid themed('backgroundColor')
}
font-size: 16px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
margin-bottom: -1px;
text-overflow: ellipsis;
overflow: hidden;
width: 400px;
}
.properties {
@include themify($themes) {
border: 1px solid themed('borderColor')
}
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-top-right-radius: 5px;
padding: 10px;
}
}

Просмотреть файл

@ -0,0 +1,114 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { ConnectionString, ConnectionStringProps } from './connectionString';
import { ConnectionStringDelete } from './connectionStringDelete';
describe('connectionString', () => {
const connectionString = 'HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key';
it('matches snapshot', () => {
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString: jest.fn(),
onEditConnectionString: jest.fn(),
onSelectConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionString {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('calls onSelectConnectionString when link clicked', () => {
const onSelectConnectionString = jest.fn();
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString: jest.fn(),
onEditConnectionString: jest.fn(),
onSelectConnectionString
};
const wrapper = shallow(<ConnectionString {...props}/>);
wrapper.find(Link).props().onClick(undefined);
expect(onSelectConnectionString).toHaveBeenCalledWith(connectionString, 'test.azure-devices-int.net');
});
it('calls onEditConnectionString when edit button clicked', () => {
const onEditConnectionString = jest.fn();
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString: jest.fn(),
onEditConnectionString,
onSelectConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionString {...props}/>);
wrapper.find(IconButton).first().props().onClick(undefined);
expect(onEditConnectionString).toHaveBeenCalledWith(connectionString);
});
describe('delete scenario', () => {
it('launches delete confirmation when delete clicked', () => {
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString: jest.fn(),
onEditConnectionString: jest.fn(),
onSelectConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionString {...props}/>);
wrapper.find(IconButton).get(1).props.onClick(undefined);
wrapper.update();
const connectionStringDelete = wrapper.find(ConnectionStringDelete);
expect(connectionStringDelete.props().hidden).toEqual(false);
});
it('calls onDeleteConnectionString when ConnectionStringDelete confirmed', () => {
const onDeleteConnectionString = jest.fn();
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString,
onEditConnectionString: jest.fn(),
onSelectConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionString {...props}/>);
wrapper.find(IconButton).get(1).props.onClick(undefined);
wrapper.update();
const connectionStringDelete = wrapper.find(ConnectionStringDelete);
connectionStringDelete.props().onDeleteConfirm();
expect (onDeleteConnectionString).toHaveBeenCalledWith(connectionString);
});
it('hides delete confirmation when ConnnectionStringDelete canceled', () => {
const props: ConnectionStringProps = {
connectionString,
onDeleteConnectionString: jest.fn(),
onEditConnectionString: jest.fn(),
onSelectConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionString {...props}/>);
wrapper.find(IconButton).get(1).props.onClick(undefined);
wrapper.update();
const connectionStringDelete = wrapper.find(ConnectionStringDelete);
expect(connectionStringDelete.props().hidden).toEqual(false);
connectionStringDelete.props().onDeleteCancel();
wrapper.update();
const updatedConnectionStringDelete = wrapper.find(ConnectionStringDelete);
expect(updatedConnectionStringDelete.props().hidden).toEqual(true);
});
});
});

Просмотреть файл

@ -0,0 +1,108 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { getConnectionInfoFromConnectionString } from '../../api/shared/utils';
import { getResourceNameFromHostName } from '../../api/shared/hostNameUtils';
import { ConnectionStringProperties } from './connectionStringProperties';
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../localization/resourceKeys';
import { ConnectionStringDelete } from './connectionStringDelete';
import MaskedCopyableTextFieldContainer from '../../shared/components/maskedCopyableTextFieldContainer';
import './connectionString.scss';
export interface ConnectionStringProps {
connectionString: string;
onEditConnectionString(connectionString: string): void;
onDeleteConnectionString(connectionString: string): void;
onSelectConnectionString(connectionString: string, hostName: string): void;
}
export const ConnectionString: React.FC<ConnectionStringProps> = props => {
const { connectionString, onEditConnectionString, onDeleteConnectionString, onSelectConnectionString } = props;
const connectionSettings = getConnectionInfoFromConnectionString(connectionString);
const { hostName, sharedAccessKey, sharedAccessKeyName } = connectionSettings;
const resourceName = getResourceNameFromHostName(hostName);
const [ confirmingDelete, setConfirmingDelete ] = React.useState<boolean>(false);
const { t } = useLocalizationContext();
const onEditConnectionStringClick = () => {
onEditConnectionString(connectionString);
};
const onDeleteConnectionStringClick = () => {
setConfirmingDelete(true);
};
const onDeleteConnectionStringConfirm = () => {
setConfirmingDelete(false);
onDeleteConnectionString(connectionString);
};
const onDeleteConnectionStringCancel = () => {
setConfirmingDelete(false);
};
const onSelectConnectionStringClick = () => {
onSelectConnectionString(connectionString, hostName);
};
return (
<div className="connection-string">
<div className="commands">
<div className="name">
<Link
ariaLabel={t(ResourceKeys.connectionStrings.visitConnectionCommand.ariaLabel, {connectionString})}
onClick={onSelectConnectionStringClick}
title={resourceName}
>
{resourceName}
</Link>
</div>
<div className="actions">
<IconButton
iconProps={{
iconName: 'EditSolid12'
}}
title={t(ResourceKeys.connectionStrings.editConnectionCommand.label)}
ariaLabel={t(ResourceKeys.connectionStrings.editConnectionCommand.ariaLabel, {connectionString})}
onClick={onEditConnectionStringClick}
/>
<IconButton
iconProps={{
iconName: 'Delete'
}}
title={t(ResourceKeys.connectionStrings.deleteConnectionCommand.label)}
ariaLabel={t(ResourceKeys.connectionStrings.deleteConnectionCommand.ariaLabel, {connectionString})}
onClick={onDeleteConnectionStringClick}
/>
</div>
</div>
<div className="properties">
<ConnectionStringProperties
connectionString={connectionString}
hostName={hostName}
sharedAccessKey={sharedAccessKey}
sharedAccessKeyName={sharedAccessKeyName}
/>
<MaskedCopyableTextFieldContainer
ariaLabel={t(ResourceKeys.connectionStrings.properties.connectionString.ariaLabel)}
allowMask={false}
label={t(ResourceKeys.connectionStrings.properties.connectionString.label)}
value={connectionString}
readOnly={true}
/>
</div>
<ConnectionStringDelete
connectionString={connectionString}
hidden={!confirmingDelete}
onDeleteCancel={onDeleteConnectionStringCancel}
onDeleteConfirm={onDeleteConnectionStringConfirm}
/>
</div>
);
};

Просмотреть файл

@ -0,0 +1,20 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
@import '../../css/themes';
@import '../../css/variables';
.connection-string-delete {
textArea {
resize: vertical;
margin-top: 5px;
margin-bottom: 5px;
@include themify($themes) {
border: 1px solid themed('borderColor');
background-color: themed('backgroundColor');
color: themed('textColor');
}
}
}

Просмотреть файл

@ -0,0 +1,63 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow } from 'enzyme';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { ConnectionStringDelete, ConnectionStringDeleteProps } from './connectionStringDelete';
describe('ConnectionStringDelete', () => {
it('matches snapshot hidden', () => {
const props: ConnectionStringDeleteProps = {
connectionString: 'connectionString',
hidden: true,
onDeleteCancel: jest.fn(),
onDeleteConfirm: jest.fn(),
};
const wrapper = shallow(<ConnectionStringDelete {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot visible', () => {
const props: ConnectionStringDeleteProps = {
connectionString: 'connectionString',
hidden: false,
onDeleteCancel: jest.fn(),
onDeleteConfirm: jest.fn(),
};
const wrapper = shallow(<ConnectionStringDelete {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('calls onDeleteCancel when Cancel clicked', () => {
const onDeleteCancel = jest.fn();
const props: ConnectionStringDeleteProps = {
connectionString: 'connectionString',
hidden: false,
onDeleteCancel,
onDeleteConfirm: jest.fn(),
};
const wrapper = shallow(<ConnectionStringDelete {...props}/>);
wrapper.find(DefaultButton).props().onClick(undefined);
expect(onDeleteCancel).toHaveBeenCalled();
});
it('calls onDeleteConfirm when Confirm clicked', () => {
const onDeleteConfirm = jest.fn();
const props: ConnectionStringDeleteProps = {
connectionString: 'connectionString',
hidden: false,
onDeleteCancel: jest.fn(),
onDeleteConfirm
};
const wrapper = shallow(<ConnectionStringDelete {...props}/>);
wrapper.find(PrimaryButton).props().onClick(undefined);
expect(onDeleteConfirm).toHaveBeenCalled();
});
});

Просмотреть файл

@ -0,0 +1,62 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Dialog, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../localization/resourceKeys';
import './connectionStringDelete.scss';
const ROWS_FOR_CONNECTION = 8;
const COLS_FOR_CONNECTION = 40;
export interface ConnectionStringDeleteProps {
connectionString: string;
hidden: boolean;
onDeleteConfirm(): void;
onDeleteCancel(): void;
}
export const ConnectionStringDelete: React.FC<ConnectionStringDeleteProps> = props => {
const { connectionString, hidden, onDeleteCancel, onDeleteConfirm } = props;
const { t } = useLocalizationContext();
return (
<Dialog
hidden={hidden}
onDismiss={onDeleteCancel}
dialogContentProps={{
title: t(ResourceKeys.connectionStrings.deleteConnection.title)
}}
modalProps={{
isBlocking: true
}}
>
<div className="connection-string-delete">
<div>{t(ResourceKeys.connectionStrings.deleteConnection.body)}</div>
<textarea
readOnly={true}
aria-label={t(ResourceKeys.connectionStrings.deleteConnection.input)}
cols={COLS_FOR_CONNECTION}
rows={ROWS_FOR_CONNECTION}
>
{connectionString}
</textarea>
</div>
<DialogFooter>
<PrimaryButton
onClick={onDeleteConfirm}
ariaLabel={t(ResourceKeys.connectionStrings.deleteConnection.yes.ariaLabel, {connectionString})}
text={t(ResourceKeys.connectionStrings.deleteConnection.yes.label)}
/>
<DefaultButton
onClick={onDeleteCancel}
ariaLabel={t(ResourceKeys.connectionStrings.deleteConnection.no.ariaLabel, {connectionString})}
text={t(ResourceKeys.connectionStrings.deleteConnection.no.label)}
/>
</DialogFooter>
</Dialog>
);
};

Просмотреть файл

@ -0,0 +1,27 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.connection-string-edit-header {
margin-left: 20px;
margin-right: 20px;
}
.connection-string-edit-body {
margin-right: 20px;
margin-left: 20px;
.details {
margin-top: 10px;
}
}
.connection-string-edit-footer {
display: flex;
margin-left: 20px;
margin-right: 20px;
margin-bottom: 10px;
button {
margin-right: 10px;
}
}

Просмотреть файл

@ -0,0 +1,146 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow, mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { ConnectionStringEditView, ConnectionStringEditViewProps } from './connectionStringEditView';
import configureStore from '../../../app/shared/redux/store/configureStore';
describe('ConnectionStringEdit', () => {
const connectionString = 'HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key';
it('matches snapshot in Add Scenario', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: '',
connectionStrings: [],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = shallow(<ConnectionStringEditView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot in Edit Scenario', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: 'connectionString',
connectionStrings: [],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = shallow(<ConnectionStringEditView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot in Edit / invalid scenario', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: 'connectionString',
connectionStrings: [],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = shallow(<ConnectionStringEditView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot in Edit / valid scenario', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: connectionString,
connectionStrings: [],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = shallow(<ConnectionStringEditView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('calls onDismiss when Cancel button clicked', () => {
const onDismiss = jest.fn();
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: 'connectionString',
connectionStrings: [],
onCommit: jest.fn(),
onDismiss
};
const wrapper = mount(<ConnectionStringEditView {...props}/>);
wrapper.find(DefaultButton).get(1).props.onClick(undefined);
expect(onDismiss).toHaveBeenCalled();
});
describe('edit scenario', () => {
it('disables commit when validation fails', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: connectionString,
connectionStrings: [],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = mount(
<Provider store={configureStore()}>
<ConnectionStringEditView {...props}/>
</Provider>
);
act(() => wrapper.find(TextField).props().onChange(undefined, 'badConnectionString'));
wrapper.update();
const disabled = wrapper.find(PrimaryButton).props().disabled;
expect(disabled).toEqual(true);
});
it('disables commit when duplicate validation', () => {
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: '',
connectionStrings: [connectionString],
onCommit: jest.fn(),
onDismiss: jest.fn()
};
const wrapper = mount(
<Provider store={configureStore()}>
<ConnectionStringEditView {...props}/>
</Provider>
);
act(() => wrapper.find(TextField).props().onChange(undefined, connectionString));
wrapper.update();
const disabled = wrapper.find(PrimaryButton).props().disabled;
expect(disabled).toEqual(true);
});
it('calls onCommit when validation passes', () => {
const onCommit = jest.fn();
const props: ConnectionStringEditViewProps = {
connectionStringUnderEdit: '',
connectionStrings: [],
onCommit,
onDismiss: jest.fn()
};
const wrapper = mount(
<Provider store={configureStore()}>
<ConnectionStringEditView {...props}/>
</Provider>
);
act(() => wrapper.find(TextField).props().onChange(undefined, connectionString));
wrapper.update();
const commitButton = wrapper.find(PrimaryButton);
expect(commitButton.props().disabled).toEqual(false);
commitButton.props().onClick(undefined);
expect(onCommit).toHaveBeenCalled();
});
});
});

Просмотреть файл

@ -0,0 +1,152 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { ConnectionStringProperties } from './connectionStringProperties';
import { getConnectionInfoFromConnectionString } from '../../api/shared/utils';
import { generateConnectionStringValidationError } from '../../shared/utils/hubConnectionStringHelper';
import { IoTHubConnectionSettings } from '../../api/services/devicesService';
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../localization/resourceKeys';
import './connectionStringEditView.scss';
const LINES_FOR_CONNECTION = 8;
export interface ConnectionStringEditViewProps {
connectionStringUnderEdit?: string;
connectionStrings: string[];
onDismiss(): void;
onCommit(newConnectionString: string): void;
}
export const ConnectionStringEditView: React.FC<ConnectionStringEditViewProps> = props => {
const {connectionStringUnderEdit, connectionStrings, onDismiss, onCommit} = props;
const [connectionString, setConnectionString] = React.useState<string>(connectionStringUnderEdit);
const [connectionStringValidationKey, setConnectionStringValidationKey] = React.useState<string>(undefined);
const [connectionSettings, setConnectionSettings] = React.useState<IoTHubConnectionSettings>(undefined);
const { t } = useLocalizationContext();
React.useEffect(() => {
if (connectionString) {
validateConnectionString(connectionString);
}
}, []); // tslint:disable-line:align
const onConnectionStringChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
setConnectionString(newValue);
validateConnectionString(newValue);
};
const onCommitClick = () => {
onCommit(connectionString);
};
const onDismissClick = () => {
onDismiss();
};
const validateConnectionString = (updatedConnectionString: string) => {
let validationKey = generateConnectionStringValidationError(updatedConnectionString) || '';
if (!validationKey) {
const extractedConnectionSettings = getConnectionInfoFromConnectionString(updatedConnectionString);
setConnectionSettings(extractedConnectionSettings);
}
// check for duplicates and validate || after setting values (so properties display)
validationKey = (connectionStrings.indexOf(updatedConnectionString) >= 0 && updatedConnectionString !== connectionStringUnderEdit) ?
ResourceKeys.connectionStrings.editConnection.validations.duplicate :
validationKey;
setConnectionStringValidationKey(validationKey);
};
const showProperties = (): boolean => {
if (!connectionSettings) {
return false;
}
if (connectionStringValidationKey && connectionStringValidationKey !== ResourceKeys.connectionStrings.editConnection.validations.duplicate) {
return false;
}
return true;
};
const renderHeader = (): JSX.Element => {
return (
<h2 className="connection-string-edit-header">
{connectionStringUnderEdit ?
t(ResourceKeys.connectionStrings.editConnection.title.edit) :
t(ResourceKeys.connectionStrings.editConnection.title.add)
}
</h2>
);
};
const renderFooter = (): JSX.Element => {
return (
<div className="connection-string-edit-footer">
<PrimaryButton
text={t(ResourceKeys.connectionStrings.editConnection.save.label)}
ariaLabel={t(ResourceKeys.connectionStrings.editConnection.save.ariaLabel)}
onClick={onCommitClick}
disabled={connectionStringValidationKey !== ''}
/>
<DefaultButton
text={t(ResourceKeys.connectionStrings.editConnection.cancel.label)}
ariaLabel={connectionStringUnderEdit ?
t(ResourceKeys.connectionStrings.editConnection.cancel.ariaLabel.edit) :
t(ResourceKeys.connectionStrings.editConnection.cancel.ariaLabel.add)
}
onClick={onDismissClick}
/>
</div>
);
};
return (
<Panel
isOpen={true}
type={PanelType.medium}
isBlocking={true}
isFooterAtBottom={true}
onRenderHeader={renderHeader}
onRenderFooter={renderFooter}
onDismiss={onDismiss}
closeButtonAriaLabel={
connectionStringUnderEdit ?
t(ResourceKeys.connectionStrings.editConnection.cancel.ariaLabel.edit) :
t(ResourceKeys.connectionStrings.editConnection.cancel.ariaLabel.add)
}
>
<div className="connection-string-edit-body">
<TextField
ariaLabel={t(ResourceKeys.connectionStrings.editConnection.editField.ariaLabel)}
label={t(ResourceKeys.connectionStrings.editConnection.editField.label)}
onChange={onConnectionStringChange}
multiline={true}
rows={LINES_FOR_CONNECTION}
errorMessage={t(connectionStringValidationKey)}
value={connectionString}
required={true}
placeholder={t(ResourceKeys.connectionStrings.editConnection.editField.placeholder)}
/>
{showProperties() &&
<div className="details">
<ConnectionStringProperties
connectionString={connectionString}
hostName={connectionSettings.hostName}
sharedAccessKey={connectionSettings.sharedAccessKey}
sharedAccessKeyName={connectionSettings.sharedAccessKeyName}
/>
</div>
}
</div>
</Panel>
);
};

Просмотреть файл

@ -0,0 +1,21 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow } from 'enzyme';
import { ConnectionStringProperties, ConnectionStringPropertiesProps } from './connectionStringProperties';
describe('ConnectionSTringProperties', () => {
it('matches snapshot', () => {
const props: ConnectionStringPropertiesProps = {
connectionString: 'connectionString',
hostName: 'hostName',
sharedAccessKey: 'sharedAccessKey',
sharedAccessKeyName: 'sharedAccessKeyName'
};
const wrapper = shallow(<ConnectionStringProperties {...props}/>);
expect(wrapper).toMatchSnapshot();
});
});

Просмотреть файл

@ -0,0 +1,49 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import MaskedCopyableTextFieldContainer from '../../shared/components/maskedCopyableTextFieldContainer';
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../localization/resourceKeys';
export interface ConnectionStringPropertiesProps {
connectionString: string;
hostName: string;
sharedAccessKey: string;
sharedAccessKeyName: string;
}
export const ConnectionStringProperties: React.FC<ConnectionStringPropertiesProps> = props => {
const { connectionString, hostName, sharedAccessKey, sharedAccessKeyName} = props;
const { t } = useLocalizationContext();
return (
<>
<MaskedCopyableTextFieldContainer
ariaLabel={t(ResourceKeys.connectionStrings.properties.hostName.ariaLabel, {connectionString})}
allowMask={false}
label={t(ResourceKeys.connectionStrings.properties.hostName.label)}
value={hostName}
readOnly={true}
/>
<MaskedCopyableTextFieldContainer
ariaLabel={t(ResourceKeys.connectionStrings.properties.sharedAccessPolicyName.ariaLabel, {connectionString})}
allowMask={false}
label={t(ResourceKeys.connectionStrings.properties.sharedAccessPolicyName.label)}
value={sharedAccessKeyName}
readOnly={true}
/>
<MaskedCopyableTextFieldContainer
ariaLabel={t(ResourceKeys.connectionStrings.properties.sharedAccessPolicyKey.ariaLabel, {connectionString})}
allowMask={false}
label={t(ResourceKeys.connectionStrings.properties.sharedAccessPolicyKey.label)}
value={sharedAccessKey}
readOnly={true}
/>
</>
);
};

Просмотреть файл

@ -0,0 +1,9 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
.connection-strings {
display: flex;
flex-wrap: wrap;
}

Просмотреть файл

@ -0,0 +1,159 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { shallow, mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { CommandBarButton } from 'office-ui-fabric-react/lib/Button';
import { ConnectionStringsView, ConnectionStringsViewProps } from './connectionStringsView';
import { ConnectionString } from './connectionString';
import { ConnectionStringEditView } from './connectionStringEditView';
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
describe('ConnectionStringsView', () => {
it('matches snapshot when no connection strings', () => {
const props: ConnectionStringsViewProps = {
connectionStrings: [],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionStringsView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot when connection strings present', () => {
const props: ConnectionStringsViewProps = {
connectionStrings: ['connectionString1'],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionStringsView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot when connection string count exceeds max', () => {
const connectionStrings = new Array(CONNECTION_STRING_LIST_MAX_LENGTH + 1).map((s, i) => `connectionString${i}`);
const props: ConnectionStringsViewProps = {
connectionStrings,
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionStringsView {...props}/>);
expect(wrapper).toMatchSnapshot();
});
describe('add scenario', () => {
it('mounts edit view when add command clicked', () => {
const props: ConnectionStringsViewProps = {
connectionStrings: [],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = mount(<ConnectionStringsView {...props}/>);
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
act(() => {
wrapper.find(CommandBarButton).props().onClick(undefined);
});
wrapper.update();
expect(wrapper.find(ConnectionStringEditView).length).toEqual(1);
});
it('dismisses when edit view dismissed', () => {
const props: ConnectionStringsViewProps = {
connectionStrings: [],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = mount(<ConnectionStringsView {...props}/>);
act(() => {
wrapper.find(CommandBarButton).props().onClick(undefined);
});
wrapper.update();
const connectionStringEditView = wrapper.find(ConnectionStringEditView).first();
act(() => connectionStringEditView.props().onDismiss());
wrapper.update();
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
});
it('upserts when edit view applied', () => {
const upsertSpy = jest.fn();
const props: ConnectionStringsViewProps = {
connectionStrings: [],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: upsertSpy
};
const wrapper = mount(<ConnectionStringsView {...props}/>);
act(() => {
wrapper.find(CommandBarButton).props().onClick(undefined);
});
wrapper.update();
const connectionStringEditView = wrapper.find(ConnectionStringEditView).first();
act(() => connectionStringEditView.props().onCommit('newConnectionString'));
wrapper.update();
expect(upsertSpy).toHaveBeenCalledWith('newConnectionString', '');
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
});
});
describe('edit scenario', () => {
const connectionString = 'HostName=test.azure-devices-int.net;SharedAccessKeyName=iothubowner;SharedAccessKey=key';
it('mounts edit view when add command clicked', () => {
const props: ConnectionStringsViewProps = {
connectionStrings: [connectionString],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: jest.fn()
};
const wrapper = shallow(<ConnectionStringsView {...props}/>);
act(() => wrapper.find(ConnectionString).props().onEditConnectionString(connectionString));
wrapper.update();
expect(wrapper.find(ConnectionStringEditView).length).toEqual(1);
});
it('upserts when edit view applied', () => {
const upsertSpy = jest.fn();
const props: ConnectionStringsViewProps = {
connectionStrings: [connectionString],
onDeleteConnectionString: jest.fn(),
onSelectConnectionString: jest.fn(),
onUpsertConnectionString: upsertSpy
};
const wrapper = shallow(<ConnectionStringsView {...props}/>);
act(() => wrapper.find(ConnectionString).first().props().onEditConnectionString(connectionString));
wrapper.update();
const connectionStringEditView = wrapper.find(ConnectionStringEditView).first();
act(() => connectionStringEditView.props().onCommit('newConnectionString'));
wrapper.update();
expect(upsertSpy).toHaveBeenCalledWith('newConnectionString', connectionString);
expect(wrapper.find(ConnectionStringEditView).length).toEqual(0);
});
});
});

Просмотреть файл

@ -0,0 +1,124 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RouteComponentProps } from 'react-router-dom';
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { ConnectionString } from './connectionString';
import { ConnectionStringEditView } from './connectionStringEditView';
import { useLocalizationContext } from '../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../localization/resourceKeys';
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
import { StateInterface } from '../../shared/redux/state';
import { upsertConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from '../actions';
import { setActiveAzureResourceByConnectionStringAction } from '../../azureResource/actions';
import { ROUTE_PARTS } from '../../constants/routes';
import { formatConnectionStrings } from '../../shared/utils/hubConnectionStringHelper';
import '../../css/_layouts.scss';
import './connectionStringsView.scss';
export interface ConnectionStringsViewProps {
connectionStrings: string[];
onDeleteConnectionString(connectionString: string): void;
onUpsertConnectionString(newConnectionString: string, connectionString: string): void;
onSelectConnectionString(connectionString: string, hostName: string): void;
}
export const ConnectionStringsView: React.FC<ConnectionStringsViewProps> = props => {
const [ connectionStringUnderEdit, setConnectionStringUnderEdit ] = React.useState<string>(undefined);
const { connectionStrings, onDeleteConnectionString, onUpsertConnectionString, onSelectConnectionString } = props;
const { t } = useLocalizationContext();
const onAddConnectionStringClick = () => {
setConnectionStringUnderEdit('');
};
const onEditConnectionStringClick = (connectionString: string) => {
setConnectionStringUnderEdit(connectionString);
};
const onConnectionStringEditCommit = (connectionString: string) => {
onUpsertConnectionString(connectionString, connectionStringUnderEdit);
setConnectionStringUnderEdit(undefined);
};
const onConnectionStringEditDismiss = () => {
setConnectionStringUnderEdit(undefined);
};
return (
<div className="view">
<div className="view-command">
<CommandBar
items={[
{
ariaLabel: t(ResourceKeys.connectionStrings.addConnectionCommand.ariaLabel),
disabled: connectionStrings.length >= CONNECTION_STRING_LIST_MAX_LENGTH,
iconProps: { iconName: 'Add' },
key: 'add',
onClick: onAddConnectionStringClick,
text: t(ResourceKeys.connectionStrings.addConnectionCommand.label)
}
]}
/>
</div>
<div className="view-content view-scroll-vertical">
<div className="connection-strings">
{connectionStrings.map(connectionString =>
<ConnectionString
key={connectionString}
connectionString={connectionString}
onEditConnectionString={onEditConnectionStringClick}
onDeleteConnectionString={onDeleteConnectionString}
onSelectConnectionString={onSelectConnectionString}
/>
)}
</div>
</div>
{connectionStringUnderEdit !== undefined &&
<ConnectionStringEditView
connectionStringUnderEdit={connectionStringUnderEdit}
connectionStrings={connectionStrings}
onDismiss={onConnectionStringEditDismiss}
onCommit={onConnectionStringEditCommit}
/>
}
</div>
);
};
export const ConnectionStringsViewContainer: React.FC<RouteComponentProps> = props => {
const connectionStrings = useSelector((state: StateInterface) => state.connectionStringsState.connectionStrings);
const dispatch = useDispatch();
const onUpsertConnectionString = (newConnectionString: string, connectionString?: string) => {
dispatch(upsertConnectionStringAction({newConnectionString, connectionString}));
};
const onDeleteConnectionString = (connectionString: string) => {
dispatch(deleteConnectionStringAction(connectionString));
};
const onSelectConnectionString = (connectionString: string, hostName: string) => {
const updatedConnectionStrings = formatConnectionStrings(connectionStrings, connectionString);
dispatch(setConnectionStringsAction(updatedConnectionStrings));
dispatch(setActiveAzureResourceByConnectionStringAction({
connectionString,
hostName
}));
props.history.push(`/${ROUTE_PARTS.RESOURCE}/${hostName}/${ROUTE_PARTS.DEVICES}`);
};
return (
<ConnectionStringsView
onUpsertConnectionString={onUpsertConnectionString}
onDeleteConnectionString={onDeleteConnectionString}
onSelectConnectionString={onSelectConnectionString}
connectionStrings={connectionStrings}
/>
);
};

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License
**********************************************************/
import { ConnectionStringsStateInterface } from './state';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from './actions';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction, upsertConnectionStringAction } from './actions';
import reducer from './reducer';
describe('addConnectionStringAction', () => {
@ -63,3 +63,33 @@ describe('setConnectionStringAction', () => {
expect(result.connectionStrings).toEqual(['connectionString3']);
});
});
describe('upsertConnectionStringAction', () => {
it('overwrites existing connection string', () => {
const initialState: ConnectionStringsStateInterface = {
connectionStrings: [
'connectionString1',
'connectionString2'
]
};
const action = upsertConnectionStringAction({ newConnectionString: 'newConnectionString2', connectionString: 'connectionString2'});
const result = reducer(initialState, action);
expect(result.connectionStrings).toHaveLength(2); // tslint:disable-line:no-magic-numbers
expect(result.connectionStrings).toEqual(['connectionString1', 'newConnectionString2']);
});
it('appends neww connection string', () => {
const initialState: ConnectionStringsStateInterface = {
connectionStrings: [
'connectionString1',
'connectionString2'
]
};
const action = upsertConnectionStringAction({ newConnectionString: 'connectionString3' });
const result = reducer(initialState, action);
expect(result.connectionStrings).toHaveLength(3); // tslint:disable-line:no-magic-numbers
expect(result.connectionStrings).toEqual(['connectionString1', 'connectionString2', 'connectionString3']);
});
});

Просмотреть файл

@ -3,7 +3,7 @@
* Licensed under the MIT License
**********************************************************/
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from './actions';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction, upsertConnectionStringAction, UpsertConnectionStringActionPayload } from './actions';
import { connectionStringsStateInitial, ConnectionStringsStateInterface } from './state';
const reducer = reducerWithInitialState<ConnectionStringsStateInterface>(connectionStringsStateInitial())
@ -23,6 +23,19 @@ const reducer = reducerWithInitialState<ConnectionStringsStateInterface>(connect
.case(setConnectionStringsAction, (state: ConnectionStringsStateInterface, payload: string[]) => {
const updatedState = {...state};
updatedState.connectionStrings = payload;
return updatedState;
})
.case(upsertConnectionStringAction, (state: ConnectionStringsStateInterface, payload: UpsertConnectionStringActionPayload) => {
const { newConnectionString, connectionString } = payload;
const updatedState = {...state};
if (connectionString) {
updatedState.connectionStrings = state.connectionStrings.map(s => s === connectionString ? newConnectionString : s);
} else {
updatedState.connectionStrings = updatedState.connectionStrings.filter(s => s !== connectionString);
updatedState.connectionStrings.push(newConnectionString);
}
return updatedState;
});

Просмотреть файл

@ -4,17 +4,19 @@
**********************************************************/
import { takeEvery, takeLatest } from 'redux-saga/effects';
import rootSaga from './sagas';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from './actions';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction, upsertConnectionStringAction } from './actions';
import { addConnectionStringSaga } from './sagas/addConnectionStringSaga';
import { deleteConnectionStringSaga } from './sagas/deleteConnectionStringSaga';
import { setConnectionStringsSaga } from './sagas/setConnectionStringsSaga';
import { upsertConnectionStringSaga } from './sagas/upsertConnectionStringSaga';
describe('connectionStrings/saga/rootSaga', () => {
it('returns specified sagas', () => {
expect(rootSaga).toEqual([
takeEvery(addConnectionStringAction, addConnectionStringSaga),
takeEvery(deleteConnectionStringAction, deleteConnectionStringSaga),
takeLatest(setConnectionStringsAction, setConnectionStringsSaga)
takeLatest(setConnectionStringsAction, setConnectionStringsSaga),
takeEvery(upsertConnectionStringAction, upsertConnectionStringSaga)
]);
});
});

Просмотреть файл

@ -3,13 +3,15 @@
* Licensed under the MIT License
**********************************************************/
import { takeEvery, takeLatest } from 'redux-saga/effects';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction } from './actions';
import { addConnectionStringAction, deleteConnectionStringAction, setConnectionStringsAction, upsertConnectionStringAction } from './actions';
import { addConnectionStringSaga } from './sagas/addConnectionStringSaga';
import { deleteConnectionStringSaga } from './sagas/deleteConnectionStringSaga';
import { setConnectionStringsSaga } from './sagas/setConnectionStringsSaga';
import { upsertConnectionStringSaga } from './sagas/upsertConnectionStringSaga';
export default [
takeEvery(addConnectionStringAction, addConnectionStringSaga),
takeEvery(deleteConnectionStringAction, deleteConnectionStringSaga),
takeLatest(setConnectionStringsAction, setConnectionStringsSaga)
takeLatest(setConnectionStringsAction, setConnectionStringsSaga),
takeEvery(upsertConnectionStringAction, upsertConnectionStringSaga)
];

Просмотреть файл

@ -0,0 +1,113 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { call } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';
import { upsertConnectionStringAction } from '../actions';
import { upsertConnectionStringSaga } from './upsertConnectionStringSaga';
import { getConnectionStrings, setConnectionStrings } from './setConnectionStringsSaga';
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
describe('upsertConnectionStringSaga', () => {
describe('adding unlisted connection string', () => {
const upsertConnectionStringSagaGenerator = cloneableGenerator(upsertConnectionStringSaga)(upsertConnectionStringAction({ newConnectionString: 'connectionString2'}));
it('returns call effect to get connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: false,
value: call(getConnectionStrings)
});
});
it('returns call effect to set connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next('connectionString1')).toEqual({
done: false,
value: call(setConnectionStrings, 'connectionString2,connectionString1')
});
});
it('finishes', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: true,
});
});
});
describe('overwriting listed connection string', () => {
const upsertConnectionStringSagaGenerator = cloneableGenerator(upsertConnectionStringSaga)(upsertConnectionStringAction({ newConnectionString: 'newConnectionString1', connectionString: 'connectionString1'));
it('returns call effect to get connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: false,
value: call(getConnectionStrings)
});
});
it('returns call effect to set connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next('connectionString1')).toEqual({
done: false,
value: call(setConnectionStrings, 'newConnectionString1')
});
});
it('finishes', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: true,
});
});
});
describe('creating new list', () => {
const upsertConnectionStringSagaGenerator = cloneableGenerator(upsertConnectionStringSaga)(upsertConnectionStringAction({ newConnectionString: 'connectionString1'}));
it('returns call effect to get connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: false,
value: call(getConnectionStrings)
});
});
it('returns call effect to set connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next(undefined)).toEqual({
done: false,
value: call(setConnectionStrings, 'connectionString1')
});
});
it('finishes', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: true,
});
});
});
describe('slice of last connection string when max length exceeded', () => {
const connectionStrings: string[] = [];
for (let i = CONNECTION_STRING_LIST_MAX_LENGTH; i > 0; i--) {
connectionStrings.push(`connectionString${i}`);
}
const connectionStringsSerialized = connectionStrings.join(',');
const newConnectionString = `connectionString${CONNECTION_STRING_LIST_MAX_LENGTH + 1}`;
const upsertConnectionStringSagaGenerator = cloneableGenerator(upsertConnectionStringSaga)(upsertConnectionStringAction({newConnectionString}));
it('returns call effect to get connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: false,
value: call(getConnectionStrings)
});
});
it('returns call effect to set connection strings', () => {
expect(upsertConnectionStringSagaGenerator.next(connectionStringsSerialized)).toEqual({
done: false,
value: call(setConnectionStrings, [newConnectionString, ...connectionStrings.splice(0, CONNECTION_STRING_LIST_MAX_LENGTH - 1)].join(','))
});
});
it('finishes', () => {
expect(upsertConnectionStringSagaGenerator.next()).toEqual({
done: true,
});
});
});
});

Просмотреть файл

@ -0,0 +1,25 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import { call } from 'redux-saga/effects';
import { Action } from 'typescript-fsa';
import { CONNECTION_STRING_LIST_MAX_LENGTH } from '../../constants/browserStorage';
import { UpsertConnectionStringActionPayload } from '../actions';
import { getConnectionStrings, setConnectionStrings } from './setConnectionStringsSaga';
export function* upsertConnectionStringSaga(action: Action<UpsertConnectionStringActionPayload>) {
const savedStrings: string = yield call(getConnectionStrings);
let updatedValue: string;
if (savedStrings) {
const savedNames = savedStrings.split(',').filter(name => name !== action.payload.connectionString); // remove duplicates
const updatedNames = [action.payload.newConnectionString, ...savedNames].slice(0, CONNECTION_STRING_LIST_MAX_LENGTH);
updatedValue = updatedNames.join(',');
}
else {
updatedValue = action.payload.newConnectionString;
}
yield call(setConnectionStrings, updatedValue);
}

Просмотреть файл

@ -47,3 +47,5 @@ export const CLEAR = 'CLEAR';
export const DELETE = 'DELETE';
export const READ = 'READ';
export const SET = 'SET';
export const UPSERT = 'UPSERT';
export const UPDATE = 'UPDATE';

Просмотреть файл

@ -18,6 +18,7 @@
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
.main {
@include themify($themes) {

Просмотреть файл

@ -8,6 +8,7 @@
body {
margin: 0;
height: 100vh;
overflow: hidden;
}
.ms-Fabric {

Просмотреть файл

@ -7,6 +7,7 @@ import * as React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import { AzureResourceViewContainer } from '../../azureResource/components/azureResourceViewContainer';
import { AzureResourcesView } from '../../azureResources/components/azureResourcesView';
import NoMatchError from './noMatchError';
import connectivityPaneContainer from '../../login/components/connectivityPaneContainer';
import { ROUTE_PARTS } from '../../constants/routes';
@ -21,7 +22,7 @@ export const Application: React.FC = props => {
<>
<Switch>
<Route path="/" component={connectivityPaneContainer} exact={true} />
<Route path={`/${ROUTE_PARTS.RESOURCE}/`} component={withApplicationFrame(AzureResourceViewContainer)} exact={true} />
<Route path={`/${ROUTE_PARTS.RESOURCE}/`} component={withApplicationFrame(AzureResourcesView)} exact={true} />
<Route path={`/${ROUTE_PARTS.RESOURCE}/:hostName`} component={withApplicationFrame(AzureResourceViewContainer)} />
<Route component={NoMatchError}/>
</Switch>

Просмотреть файл

@ -139,6 +139,83 @@
},
"value": "Value"
},
"connectionStrings": {
"addConnectionCommand": {
"label": "Add Connection",
"ariaLabel": "Add connection string"
},
"deleteConnectionCommand": {
"label": "Delete Connection",
"ariaLabel": "Delete connection string {{connectionString}}"
},
"editConnectionCommand": {
"label": "Edit Connection",
"ariaLabel": "Edit connection string {{connectionString}}"
},
"copyConnectionCommand": {
"label": "Copy Connection",
"ariaLabel": "Copy connection string {{connectionString}}"
},
"visitConnectionCommand": {
"ariaLabel": "View resource: {{connection string}}"
},
"deleteConnection": {
"title": "Confirm Delete",
"body": "Delete the following connection string?",
"input": "Connection string to delete",
"yes": {
"ariaLabel": "Yes, delete {{connectionString}}",
"label": "Yes"
},
"no": {
"ariaLabel": "No, do not delete {{connectionString}}",
"label": "No"
}
},
"editConnection": {
"save": {
"label": "Save",
"ariaLabel": "Save connection string"
},
"cancel": {
"label": "Cancel",
"ariaLabel": {
"add": "Cancel Add of Connection String",
"edit":"Cancel Edit of Connection String"
}
},
"title": {
"add": "Add Connection String",
"edit": "Edit Connection String"
},
"editField": {
"label": "Connection String",
"ariaLabel": "Connection String",
"placeholder": "enter a connection string"
},
"validations": {
"duplicate": "The specified connection string is already listed."
}
},
"properties": {
"hostName": {
"label": "Host Name",
"ariaLabel": "Host name for {{connection string}}"
},
"sharedAccessPolicyName": {
"label": "Shared Access Policy Name",
"ariaLabel": "Shared Access Policy Name for {{connection string}}"
},
"sharedAccessPolicyKey": {
"label": "Shared Access Policy Key",
"ariaLabel": "Shared Access Policy Key for {{connection string}}"
},
"connectionString": {
"label": "Connection String",
"ariaLabel": "Connection String"
}
}
},
"connectivityPane": {
"header": "App configurations",
"connectionStringComboBox": {

Просмотреть файл

@ -107,6 +107,83 @@ export class ResourceKeys {
},
},
};
public static connectionStrings = {
addConnectionCommand : {
ariaLabel : "connectionStrings.addConnectionCommand.ariaLabel",
label : "connectionStrings.addConnectionCommand.label",
},
copyConnectionCommand : {
ariaLabel : "connectionStrings.copyConnectionCommand.ariaLabel",
label : "connectionStrings.copyConnectionCommand.label",
},
deleteConnection : {
body : "connectionStrings.deleteConnection.body",
input : "connectionStrings.deleteConnection.input",
no : {
ariaLabel : "connectionStrings.deleteConnection.no.ariaLabel",
label : "connectionStrings.deleteConnection.no.label",
},
title : "connectionStrings.deleteConnection.title",
yes : {
ariaLabel : "connectionStrings.deleteConnection.yes.ariaLabel",
label : "connectionStrings.deleteConnection.yes.label",
},
},
deleteConnectionCommand : {
ariaLabel : "connectionStrings.deleteConnectionCommand.ariaLabel",
label : "connectionStrings.deleteConnectionCommand.label",
},
editConnection : {
cancel : {
ariaLabel : {
add : "connectionStrings.editConnection.cancel.ariaLabel.add",
edit : "connectionStrings.editConnection.cancel.ariaLabel.edit",
},
label : "connectionStrings.editConnection.cancel.label",
},
editField : {
ariaLabel : "connectionStrings.editConnection.editField.ariaLabel",
label : "connectionStrings.editConnection.editField.label",
placeholder : "connectionStrings.editConnection.editField.placeholder",
},
save : {
ariaLabel : "connectionStrings.editConnection.save.ariaLabel",
label : "connectionStrings.editConnection.save.label",
},
title : {
add : "connectionStrings.editConnection.title.add",
edit : "connectionStrings.editConnection.title.edit",
},
validations : {
duplicate : "connectionStrings.editConnection.validations.duplicate",
},
},
editConnectionCommand : {
ariaLabel : "connectionStrings.editConnectionCommand.ariaLabel",
label : "connectionStrings.editConnectionCommand.label",
},
properties : {
connectionString : {
ariaLabel : "connectionStrings.properties.connectionString.ariaLabel",
label : "connectionStrings.properties.connectionString.label",
},
hostName : {
ariaLabel : "connectionStrings.properties.hostName.ariaLabel",
label : "connectionStrings.properties.hostName.label",
},
sharedAccessPolicyKey : {
ariaLabel : "connectionStrings.properties.sharedAccessPolicyKey.ariaLabel",
label : "connectionStrings.properties.sharedAccessPolicyKey.label",
},
sharedAccessPolicyName : {
ariaLabel : "connectionStrings.properties.sharedAccessPolicyName.ariaLabel",
label : "connectionStrings.properties.sharedAccessPolicyName.label",
},
},
visitConnectionCommand : {
ariaLabel : "connectionStrings.visitConnectionCommand.ariaLabel",
},
};
public static connectivityPane = {
connectionStringComboBox : {
ariaLabel : "connectivityPane.connectionStringComboBox.ariaLabel",

Просмотреть файл

@ -138,6 +138,7 @@
"redux-saga/effects",
"redux-saga/utils",
"redux-devtools-extension",
"react-dom",
"react-toastify"
],
"no-switch-case-fall-through": true,