-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/persistent-docker-network #734
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React from 'react'; | ||
import { act, fireEvent } from '@testing-library/react'; | ||
import { Form } from 'antd'; | ||
import { injections, renderWithProviders } from 'utils/tests'; | ||
import DockerNetworkName from './DockerNetworkName'; | ||
|
||
// Mock the useStoreActions hook | ||
const mockDockerService = injections.dockerService as jest.Mocked< | ||
typeof injections.dockerService | ||
>; | ||
|
||
describe('DockerNetworkName', () => { | ||
let unmount: () => void; | ||
let result: any; | ||
const mockValidateCallback = jest.fn(); | ||
|
||
const renderComponent = async () => { | ||
await act(async () => { | ||
result = renderWithProviders( | ||
<Form> | ||
<DockerNetworkName name="network" validateCallback={mockValidateCallback} /> | ||
</Form>, | ||
); | ||
unmount = result.unmount; | ||
//return result; | ||
}); | ||
}; | ||
|
||
beforeEach(() => { | ||
mockDockerService.getDockerExternalNetworks.mockResolvedValue([ | ||
'test-network', | ||
'test-network-2', | ||
]); | ||
}); | ||
|
||
afterEach(() => unmount()); | ||
|
||
describe('When the modal renders', () => { | ||
it('should display External Docker Network label', async () => { | ||
await renderComponent(); | ||
const { getByText } = result; | ||
expect(getByText('External Docker Network')).toBeInTheDocument(); | ||
expect(getByText('Select a network leave blank to clear')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should fetch external docker networks on mount', async () => { | ||
await renderComponent(); | ||
expect(mockDockerService.getDockerExternalNetworks).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('When the form item validates', () => { | ||
it('should show help message for creating an external docker network', async () => { | ||
await renderComponent(); | ||
|
||
const { getByLabelText, findByText, container } = result; | ||
const input = getByLabelText('External Docker Network') as HTMLInputElement; | ||
fireEvent.change(input, { target: { value: 'test-network-3' } }); | ||
expect( | ||
await findByText('Docker External Network will be created'), | ||
).toBeInTheDocument(); | ||
expect(mockValidateCallback).toHaveBeenCalledWith(true); | ||
const element = container.querySelector('.ant-select.ant-select-status-warning'); | ||
expect(element).not.toBeNull(); | ||
}); | ||
|
||
it('should show help message for attaching to a external docker network', async () => { | ||
await renderComponent(); | ||
const { getByLabelText, getByText, findByText, container } = result; | ||
|
||
expect(getByText('External Docker Network')).toBeInTheDocument(); | ||
expect(getByLabelText('External Docker Network')).toBeInstanceOf(HTMLInputElement); | ||
const input = getByLabelText('External Docker Network') as HTMLInputElement; | ||
fireEvent.change(input, { target: { value: 'test-network' } }); | ||
expect( | ||
await findByText('Docker Network will be attached to test-network'), | ||
).toBeInTheDocument(); | ||
expect(mockValidateCallback).toHaveBeenCalledWith(true); | ||
fireEvent.change(input, { target: { value: 'test-network-2' } }); | ||
expect( | ||
await findByText('Docker Network will be attached to test-network-2'), | ||
).toBeInTheDocument(); | ||
expect(mockValidateCallback).toHaveBeenCalledWith(true); | ||
const element = container.querySelector('.ant-select.ant-select-status-success'); | ||
expect(element).not.toBeNull(); | ||
}); | ||
|
||
it('should show help message for an invalid docker network name', async () => { | ||
await renderComponent(); | ||
|
||
const { getByLabelText, findByText, container } = result; | ||
const input = getByLabelText('External Docker Network') as HTMLInputElement; | ||
fireEvent.change(input, { target: { value: '__' } }); | ||
expect(await findByText('Invalid Docker Network Name')).toBeInTheDocument(); | ||
expect(mockValidateCallback).toHaveBeenCalledWith(false); | ||
const element = container.querySelector('.ant-select.ant-select-status-error'); | ||
expect(element).not.toBeNull(); | ||
}); | ||
|
||
it('should show help message for clearing the external network', async () => { | ||
await renderComponent(); | ||
|
||
const { getByLabelText, findByText, container } = result; | ||
const input = getByLabelText('External Docker Network') as HTMLInputElement; | ||
|
||
fireEvent.change(input, { target: { value: 'default' } }); | ||
expect(await findByText('Docker Network will be cleared')).toBeInTheDocument(); | ||
|
||
fireEvent.change(input, { target: { value: '' } }); | ||
expect(await findByText('Docker Network will be cleared')).toBeInTheDocument(); | ||
|
||
const element = container.querySelector('.ant-select.ant-select-status-success'); | ||
expect(element).not.toBeNull(); | ||
}); | ||
}); | ||
|
||
// Add more test cases as needed | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import { AutoComplete, Form } from 'antd'; | ||
import { usePrefixedTranslation } from 'hooks'; | ||
import { useStoreActions } from 'store'; | ||
|
||
type Status = '' | 'warning' | 'error' | undefined; | ||
|
||
interface Props { | ||
name: string; | ||
defaultValue?: string; | ||
validateCallback?: (value: boolean) => void; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of bubbling up the validation status via props, you could use <Form.Item rules={[{ pattern: /^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,63}$/, message: l('helpInvalid') }]}> |
||
} | ||
|
||
const DockerNetworkName: React.FC<Props> = ({ name, defaultValue, validateCallback }) => { | ||
const { l } = usePrefixedTranslation('cmps.common.form.DockerNetworkName'); | ||
|
||
const { getExternalDockerNetworks } = useStoreActions(s => s.network); | ||
|
||
const [dockerNetworks, setDockerNetworks] = useState<string[]>([]); | ||
const [status, setStatus] = useState<Status>(undefined); | ||
const [help, setHelp] = useState<string>(''); | ||
|
||
const validate = (isValid: boolean) => { | ||
validateCallback && validateCallback(isValid); | ||
}; | ||
|
||
useEffect(() => { | ||
(async () => { | ||
const networks = await getExternalDockerNetworks(); | ||
setDockerNetworks(networks); | ||
})(); | ||
}, []); | ||
|
||
const validateNetworkName = async (value: string) => { | ||
if (value.length === 0 || value === 'default') { | ||
setHelp(l('helpClear')); | ||
setStatus(undefined); | ||
validate(true); | ||
return; | ||
} | ||
const regex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,63}$/; | ||
if (regex.test(value)) { | ||
if (!dockerNetworks.includes(value)) { | ||
setHelp(l('helpCreate')); | ||
setStatus('warning'); | ||
validate(true); | ||
} else { | ||
setHelp(l('helpAttach', { dockerNetwork: value })); | ||
setStatus(undefined); | ||
validate(true); | ||
} | ||
} else { | ||
setHelp(l('helpInvalid')); | ||
setStatus('error'); | ||
validate(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<Form.Item name={name} label={l('label')} help={help}> | ||
<AutoComplete | ||
options={dockerNetworks?.map(network => ({ | ||
value: network, | ||
}))} | ||
placeholder={l('placeholder')} | ||
filterOption={(inputValue, option) => | ||
option?.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 | ||
} | ||
onChange={validateNetworkName} | ||
status={status} | ||
defaultValue={defaultValue} | ||
/> | ||
</Form.Item> | ||
); | ||
}; | ||
|
||
export default DockerNetworkName; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,33 +3,37 @@ import { Status } from 'shared/types'; | |
import { renderWithProviders } from 'utils/tests'; | ||
import StatusTag from './StatusTag'; | ||
|
||
describe('StatusBadge Component', () => { | ||
describe('StatusTag Component', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why change this |
||
const renderComponent = (status: Status) => { | ||
return renderWithProviders(<StatusTag status={status} />); | ||
const result = renderWithProviders(<StatusTag status={status} />); | ||
return { | ||
...result, | ||
dot: result.container.querySelector('.ant-tag'), | ||
}; | ||
}; | ||
|
||
it('should render the Starting status', () => { | ||
const { getByText } = renderComponent(Status.Starting); | ||
expect(getByText('Starting')).toBeInTheDocument(); | ||
const { dot } = renderComponent(Status.Starting); | ||
expect(dot).toHaveClass('ant-tag-blue'); | ||
}); | ||
|
||
it('should render the Started status', () => { | ||
const { getByText } = renderComponent(Status.Started); | ||
expect(getByText('Started')).toBeInTheDocument(); | ||
const { dot } = renderComponent(Status.Started); | ||
expect(dot).toHaveClass('ant-tag-green'); | ||
}); | ||
|
||
it('should render the Stopping status', () => { | ||
const { getByText } = renderComponent(Status.Stopping); | ||
expect(getByText('Stopping')).toBeInTheDocument(); | ||
const { dot } = renderComponent(Status.Stopping); | ||
expect(dot).toHaveClass('ant-tag-blue'); | ||
}); | ||
|
||
it('should render the Stopped status', () => { | ||
const { getByText } = renderComponent(Status.Stopped); | ||
expect(getByText('Stopped')).toBeInTheDocument(); | ||
const { dot } = renderComponent(Status.Stopped); | ||
expect(dot).toHaveClass('ant-tag-red'); | ||
}); | ||
|
||
it('should render the Error status', () => { | ||
const { getByText } = renderComponent(Status.Error); | ||
expect(getByText('Error')).toBeInTheDocument(); | ||
const { dot } = renderComponent(Status.Error); | ||
expect(dot).toHaveClass('ant-tag-red'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,6 @@ | ||
import React from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Tag } from 'antd'; | ||
import { useTheme } from 'hooks/useTheme'; | ||
import { Status } from 'shared/types'; | ||
|
||
export interface StatusTagProps { | ||
|
@@ -11,13 +10,11 @@ export interface StatusTagProps { | |
const StatusTag: React.FC<StatusTagProps> = ({ status }) => { | ||
const { t } = useTranslation(); | ||
|
||
const { statusTag } = useTheme(); | ||
|
||
const statusColors = { | ||
[Status.Starting]: 'blue', | ||
[Status.Started]: 'green', | ||
[Status.Stopping]: 'blue', | ||
[Status.Stopped]: statusTag.stopped, | ||
[Status.Stopped]: 'red', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be changed to red because stopped is not an error state. |
||
[Status.Error]: 'red', | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
// @jest-ignore | ||
|
||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These empty lines aren't necessary. |
||
import styled from '@emotion/styled'; | ||
import { Card } from 'antd'; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why this change was needed for you, but I got this error when trying to start the app with it:
When I removed this option it worked fine.