Skip to content

Commit

Permalink
secrets paneling polished
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-logro authored and tekton-robot committed Jul 22, 2019
1 parent 8a98cfc commit 5290855
Show file tree
Hide file tree
Showing 20 changed files with 680 additions and 580 deletions.
36 changes: 22 additions & 14 deletions src/actions/secrets.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,35 @@ export function fetchSecrets({ namespace } = {}) {
secretsFormatted.push(object);
});
dispatch(fetchSecretsSuccess(secretsFormatted));
} catch (e) {
const error = new Error('Could not fetch secrets');
} catch (error) {
dispatch({ type: 'SECRETS_FETCH_FAILURE', error });
}
return secrets;
};
}

export function deleteSecret(name, namespace) {
export function deleteSecret(secrets) {
return async dispatch => {
dispatch({ type: 'SECRET_DELETE_REQUEST' });
try {
await deleteCredential(name, namespace);
dispatch({ type: 'SECRET_DELETE_SUCCESS', name, namespace });
} catch (e) {
const error = new Error(`Could not delete secret "${name}"`);
dispatch({ type: 'SECRET_DELETE_FAILURE', error });
}
const deletePromises = secrets.map(secret => {
const { name, namespace } = secret;
const response = deleteCredential(name, namespace);
const timeout = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('An error occured deleting the secret(s).'));
}, 1000);
});
const deleteWithinTimePromise = Promise.race([response, timeout]);
return deleteWithinTimePromise;
});

Promise.all(deletePromises)
.then(() => {
dispatch({ type: 'SECRET_DELETE_SUCCESS', secrets });
})
.catch(error => {
dispatch({ type: 'SECRET_DELETE_FAILURE', error });
});
};
}

Expand All @@ -66,10 +77,7 @@ export function createSecret(postData, namespace) {
try {
await createCredential(postData, namespace);
dispatch(fetchSecrets());
} catch (e) {
const error = new Error(
`Could not create secret "${postData.name}" in namespace ${namespace}`
);
} catch (error) {
dispatch({ type: 'SECRET_CREATE_FAILURE', error });
}
};
Expand Down
23 changes: 9 additions & 14 deletions src/actions/secrets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ it('fetchSecrets error', async () => {
});

it('deleteSecret', async () => {
const name = 'secret-name';
const secrets = [{ name: 'secret-name', namespace: 'default' }];
const middleware = [thunk];
const mockStore = configureStore(middleware);
const store = mockStore();
Expand All @@ -117,31 +117,26 @@ it('deleteSecret', async () => {

const expectedActions = [
{ type: 'SECRET_DELETE_REQUEST' },
{ type: 'SECRET_DELETE_SUCCESS', name, namespace }
{ type: 'SECRET_DELETE_SUCCESS', secrets }
];

await store.dispatch(deleteSecret(name, namespace));
await store.dispatch(deleteSecret(secrets));
expect(store.getActions()).toEqual(expectedActions);
});

it('deleteSecret error', async () => {
const secret = 'secret';
const secrets = [{ name: 'secret-name', namespace: 'default' }];
const middleware = [thunk];
const mockStore = configureStore(middleware);
const store = mockStore();

const error = new Error('Could not delete secret "secret"');

jest.spyOn(API, 'deleteCredential').mockImplementation(() => {
throw error;
});
jest
.spyOn(API, 'deleteCredential')
.mockImplementation(() => Promise.reject());

const expectedActions = [
{ type: 'SECRET_DELETE_REQUEST' },
{ type: 'SECRET_DELETE_FAILURE', error }
];
const expectedActions = [{ type: 'SECRET_DELETE_REQUEST' }];

await store.dispatch(deleteSecret(secret, namespace));
await store.dispatch(deleteSecret(secrets));
expect(store.getActions()).toEqual(expectedActions);
});

Expand Down
12 changes: 8 additions & 4 deletions src/components/SecretsDeleteModal/SecretsDeleteModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Modal } from 'carbon-components-react';
import './SecretsDeleteModal.scss';

const SecretsDeleteModal = props => {
const { open, id, handleClick, handleDelete } = props;
const { open, toBeDeleted, handleClick, handleDelete } = props;

return (
<Modal
Expand All @@ -29,9 +29,13 @@ const SecretsDeleteModal = props => {
onRequestSubmit={handleDelete}
onRequestClose={handleClick}
>
<p>
Are you sure you want to delete the secret <strong>{id}</strong>?
</p>
<p>Are you sure you want to delete these secrets?</p>
<ul>
{toBeDeleted.map(secret => {
const { name, namespace } = secret;
return <li key={`${name}:${namespace}`}>{name}</li>;
})}
</ul>
</Modal>
);
};
Expand Down
12 changes: 9 additions & 3 deletions src/components/SecretsDeleteModal/SecretsDeleteModal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ limitations under the License.
line-height: 1.75rem;
}

strong {
font-weight: bold;
color: $support-01;
ul {
list-style: disc;
padding: 5px 0 0 20px;
li {
font-weight: bold;
color: $support-01;
font-size: 1.25rem;
margin-bottom: 5px;
}
}

.bx--modal-footer {
Expand Down
30 changes: 25 additions & 5 deletions src/components/SecretsDeleteModal/SecretsDeleteModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,35 @@ import React from 'react';
import { fireEvent, render } from 'react-testing-library';
import SecretsDeleteModal from './SecretsDeleteModal';

it('SecretsDeleteModal renders with passed secret id', () => {
it('SecretsDeleteModal renders with one passed secret', () => {
const props = {
open: true,
id: 'dummySecret',
toBeDeleted: [{ name: 'secret-name', namespace: 'default' }],
handleClick() {},
handleDelete() {}
};
const { queryByText } = render(<SecretsDeleteModal {...props} />);
expect(queryByText('dummySecret')).toBeTruthy();
expect(queryByText('secret-name')).toBeTruthy();
expect(queryByText('Cancel')).toBeTruthy();
expect(queryByText('Delete')).toBeTruthy();
expect(queryByText('Delete Secret')).toBeTruthy();
});

it('SecretsDeleteModal renders with multiple passed secrets', () => {
const props = {
open: true,
toBeDeleted: [
{ name: 'secret-name', namespace: 'default' },
{ name: 'other-secret', namespace: 'default' },
{ name: 'another-one', namespace: 'default' }
],
handleClick() {},
handleDelete() {}
};
const { queryByText } = render(<SecretsDeleteModal {...props} />);
expect(queryByText('secret-name')).toBeTruthy();
expect(queryByText('other-secret')).toBeTruthy();
expect(queryByText('another-one')).toBeTruthy();
expect(queryByText('Cancel')).toBeTruthy();
expect(queryByText('Delete')).toBeTruthy();
expect(queryByText('Delete Secret')).toBeTruthy();
Expand All @@ -34,15 +54,15 @@ it('Test SecretsDeleteModal click events', () => {
const handleDelete = jest.fn();
const props = {
open: true,
id: 'dummySecret',
toBeDeleted: [{ name: 'secret-name', namespace: 'default' }],
handleClick,
handleDelete
};

const { queryByText, rerender } = render(<SecretsDeleteModal {...props} />);
fireEvent.click(queryByText('Delete'));
expect(handleDelete).toHaveBeenCalledTimes(1);
rerender(<SecretsDeleteModal open={false} />);
rerender(<SecretsDeleteModal {...props} open={false} />);
fireEvent.click(queryByText('Delete'));
expect(handleClick).toHaveBeenCalledTimes(0);
});
2 changes: 2 additions & 0 deletions src/components/SecretsModal/Annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const Annotations = props => {
<Remove className="removeIcon" onClick={handleRemove} />
<Add className="addIcon" onClick={handleAdd} />
</div>
{invalidFields.find(field => field.includes('annotation-value')) !==
undefined && <p className="invalidAnnotation">Required.</p>}
{annotationFields}
</div>
);
Expand Down
27 changes: 16 additions & 11 deletions src/components/SecretsModal/BasicAuthFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ limitations under the License.
import React from 'react';
import { TextInput } from 'carbon-components-react';
import './SecretsModal.scss';
import ServiceAccountsDropdown from '../../containers/ServiceAccountsDropdown';

const BasicAuthFields = props => {
const {
username,
password,
handleChange,
namespace,
handleChangeTextInput,
handleChangeServiceAccount,
invalidFields,
serviceAccount
} = props;
Expand All @@ -31,8 +34,9 @@ const BasicAuthFields = props => {
placeholder="example@domain.com"
value={username}
labelText="Email:"
onChange={handleChange}
onChange={handleChangeTextInput}
invalid={invalidFields.indexOf('username') > -1}
invalidText="Required."
/>
<TextInput
id="password"
Expand All @@ -41,19 +45,20 @@ const BasicAuthFields = props => {
value={password}
placeholder="********"
labelText="Password/Token:"
onChange={handleChange}
onChange={handleChangeTextInput}
invalid={invalidFields.indexOf('password') > -1}
invalidText="Required."
/>
<TextInput
<ServiceAccountsDropdown
id="serviceAccount"
autoComplete="off"
type="serviceAccount"
value={serviceAccount}
placeholder="default"
labelText="Service Account:"
onChange={handleChange}
titleText="Service Account"
namespace={namespace}
selectedItem={
serviceAccount ? { id: serviceAccount, text: serviceAccount } : ''
}
onChange={handleChangeServiceAccount}
invalid={invalidFields.indexOf('serviceAccount') > -1}
invalidText="Must be less than 563 characters, contain only lowercase alphanumeric characters, . or -"
invalidText="Required."
/>
</>
);
Expand Down
95 changes: 92 additions & 3 deletions src/components/SecretsModal/BasicAuthFields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,81 @@ limitations under the License.

import React from 'react';
import { render } from 'react-testing-library';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as API from '../../api';
import BasicAuthFields from './BasicAuthFields';

const middleware = [thunk];
const mockStore = configureStore(middleware);

const namespaces = {
byName: {
default: {
metadata: {
name: 'default',
selfLink: '/api/v1/namespaces/default',
uid: '32b35d3b-6ce1-11e9-af21-025000000001',
resourceVersion: '4',
creationTimestamp: '2019-05-02T13:50:08Z'
},
spec: {
finalizers: ['kubernetes']
},
status: {
phase: 'Active'
}
}
},
errorMessage: null,
isFetching: false,
selected: 'default'
};

const serviceAccountsByNamespace = {
blue: {
'service-account-1': 'id-service-account-1',
'service-account-2': 'id-service-account-2'
},
green: {
'service-account-3': 'id-service-account-3'
}
};

const serviceAccountsById = {
'id-service-account-1': {
metadata: {
name: 'service-account-1',
namespace: 'blue',
uid: 'id-service-account-1'
}
},
'id-service-account-2': {
metadata: {
name: 'service-account-2',
namespace: 'blue',
uid: 'id-service-account-2'
}
},
'id-service-account-3': {
metadata: {
name: 'service-account-3',
namespace: 'green',
uid: 'id-service-account-3'
}
}
};

const store = mockStore({
namespaces,
serviceAccounts: {
byId: serviceAccountsById,
byNamespace: serviceAccountsByNamespace,
isFetching: false
}
});

it('BasicAuthFields renders with blank inputs', () => {
const props = {
username: '',
Expand All @@ -23,13 +96,20 @@ it('BasicAuthFields renders with blank inputs', () => {
handleChange() {},
invalidFields: []
};

jest
.spyOn(API, 'getServiceAccounts')
.mockImplementation(() => serviceAccountsById);

const { getByLabelText, getAllByDisplayValue } = render(
<BasicAuthFields {...props} />
<Provider store={store}>
<BasicAuthFields {...props} />
</Provider>
);
expect(getByLabelText(/Email/i)).toBeTruthy();
expect(getByLabelText(/Password\/Token/i)).toBeTruthy();
expect(getByLabelText(/Service Account/i)).toBeTruthy();
expect(getAllByDisplayValue('').length).toEqual(3);
expect(getAllByDisplayValue('').length).toEqual(2);
});

it('BasicAuthFields incorrect fields', () => {
Expand All @@ -40,7 +120,16 @@ it('BasicAuthFields incorrect fields', () => {
handleChange() {},
invalidFields: ['username', 'password']
};
const { getByLabelText } = render(<BasicAuthFields {...props} />);

jest
.spyOn(API, 'getServiceAccounts')
.mockImplementation(() => serviceAccountsById);

const { getByLabelText } = render(
<Provider store={store}>
<BasicAuthFields {...props} />
</Provider>
);

const usernameInput = getByLabelText(/Email/i);
const passwordInput = getByLabelText(/Password\/Token/i);
Expand Down
Loading

0 comments on commit 5290855

Please sign in to comment.