* Rkessler/resource view (#14) Adds resource view. * Rename and consolidate per pr.
This commit is contained in:
Родитель
15357ce289
Коммит
be1f8b90f9
|
@ -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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче