Skip to content
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

Improve Service Discovery Errors (AKA improve New API form) #1015

Merged
merged 13 commits into from
Aug 20, 2019
Merged
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[lints]

[options]
module.system.node.resolve_dirname=node_modules
damianpm marked this conversation as resolved.
Show resolved Hide resolved
module.name_mapper.extension='scss' -> 'empty/object'
module.system=haste
module.name_mapper='\(Applications\|Dashboard\|LoginPage\|Navigation\|Onboarding\|Policies\|services\|Stats\|Types\|Users\|utilities\|NewService\)\(.*\)$' -> '<PROJECT_ROOT>/app/javascript/src/\1/\2'
18 changes: 12 additions & 6 deletions app/assets/stylesheets/provider/_forms.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
$fieldset-margin-bottom: line-height-times(1.5);

form.formtastic ol, form.formtastic ul {
list-style: none;
}
Expand Down Expand Up @@ -310,7 +312,7 @@ form.formtastic {
position: relative;
padding: line-height-times(1) 0 0 0;
border-top: $border-width solid $border-color;
margin-bottom: line-height-times(1.5);
margin-bottom: $fieldset-margin-bottom;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
Expand Down Expand Up @@ -533,10 +535,14 @@ li.hidden {
display: none;
}

.errorMessage {
color: $error-color;
}
#new_service_wrapper {
.errorMessage {
color: $error-color;
margin-bottom: line-height-times(1/2);
margin-top: -#{$fieldset-margin-bottom};
josemigallas marked this conversation as resolved.
Show resolved Hide resolved
}

.new-service-source-input {
padding-left: line-height-times(5 / 10);
.new-service-source-input {
padding: 0 line-height-times(5 / 10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Provider::Admin::ServiceDiscovery::ClusterProjectsController < Provider::Admin::ServiceDiscovery::ClusterBaseController
def index
render json: { projects: cluster.projects_with_discoverables.map(&:to_json) }
render json: cluster.projects_with_discoverables.map(&:name)
.to_json
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

class Provider::Admin::ServiceDiscovery::ClusterServicesController < Provider::Admin::ServiceDiscovery::ClusterBaseController
def index
render json: { services: cluster.discoverable_services(namespace: params.require(:namespace_id)).map(&:to_json) }
render json: cluster.discoverable_services(namespace: namespace_id).map(&:name)
.to_json
end

def show
cluster_service = cluster.find_discoverable_service_by(namespace: params.require(:namespace_id), name: params[:id])
cluster_service = cluster.find_discoverable_service_by(namespace: namespace_id, name: params[:id])
render json: cluster_service.to_json
rescue ::ServiceDiscovery::ClusterClient::ResourceNotFound => exception
render_error exception.message, status: :not_found
end

private

def namespace_id
params.require(:namespace_id)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import React from 'react'

const ErrorMessage = ({fetchErrorMessage}: {
fetchErrorMessage: string
}) => <p className='errorMessage'>
{`Sorry, your request has failed with the error: ${fetchErrorMessage}`}
</p>
}) => (
<p className='errorMessage'>
{`Sorry, your request has failed with the error: ${fetchErrorMessage}`}
</p>
)

export {
ErrorMessage
Expand Down
19 changes: 7 additions & 12 deletions app/javascript/src/NewService/components/FormElements/Select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,29 @@

import React from 'react'

type Option = {
metadata: {
name: string
}
}

type Props = {
name: string,
id: string,
disabled?: boolean,
onChange?: (event: SyntheticEvent<HTMLSelectElement>) => void,
options: Array<Option>
options: Array<string>
}

const Options = ({options}) => {
return options.map(option => {
const { name } = option.metadata
return <option key={name} value={name}>{name}</option>
return options.map((option) => {
return <option key={option} value={option}>{option}</option>
})
}

const Select = ({name, id, onChange, options}: Props) =>
const Select = ({name, id, disabled, onChange, options}: Props) =>
<select
required="required"
name={name}
id={id}
disabled={disabled}
onChange={onChange}
>
{<Options options={options}/>}
<Options options={options}/>
</select>

export {Select}
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
// @flow

import React from 'react'
import React, {useState, useEffect} from 'react'
import {Label, Select} from 'NewService/components/FormElements'
import {fetchData} from 'utilities/utils'
import {BASE_PATH} from 'NewService'

type Props = {
fetchServices: (namespace: string) => Promise<void>,
projects: string[],
services: string[]
onError: (err: string) => void
}

const ServiceDiscoveryListItems = (props: Props) => {
const {fetchServices, projects, services} = props
const { projects, onError } = props

const [services, setServices] = useState([])
const [loading, setLoading] = useState(true)

useEffect(() => {
if (projects && projects.length) {
fetchServices(projects[0])
}
}, [projects])

const fetchServices = async (namespace: string) => {
setLoading(true)
setServices([])

try {
const services = await fetchData<string[]>(`${BASE_PATH}/namespaces/${namespace}/services.json`)
setServices(services)
} catch (error) {
onError(error.message)
} finally {
setLoading(false)
}
}

return (
<React.Fragment>
<li id="service_name_input" className="string required">
<Label
htmlFor='namespace'
htmlFor='service_namespace'
label='Namespace'
/>
<Select
disabled={loading}
name='service[namespace]'
id='service_namespace'
onChange={(e) => fetchServices(e.currentTarget.value)}
onChange={e => { fetchServices(e.currentTarget.value) }}
options={projects}
/>
</li>
Expand All @@ -31,6 +57,7 @@ const ServiceDiscoveryListItems = (props: Props) => {
label='Name'
/>
<Select
disabled={loading}
name='service[name]'
id='service_name'
options={services}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow

export {FormWrapper} from 'NewService/components/FormElements/FormWrapper'
export {HiddenServiceDiscoveryInput} from 'NewService/components/FormElements/HiddenServiceDiscoveryInput'
export {Label} from 'NewService/components/FormElements/Label'
Expand Down
6 changes: 4 additions & 2 deletions app/javascript/src/NewService/components/NewServiceForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ const NewServiceForm = (props: Props) => {
providerAdminServiceDiscoveryServicesPath, adminServicesPath} = props

const [formMode, setFormMode] = useState('manual')
const [loadingProjects, setLoadingProjects] = useState(false)

const handleFormsVisibility = (event: SyntheticEvent<HTMLSelectElement>) => {
const handleFormsVisibility = (event: SyntheticEvent<HTMLInputElement>) => {
setFormMode(event.currentTarget.value)
}

const formToRender = () => formMode === 'manual' || !isServiceDiscoveryAccessible
? <ServiceManualForm formActionPath={adminServicesPath}/>
: <ServiceDiscoveryForm formActionPath={providerAdminServiceDiscoveryServicesPath}/>
: <ServiceDiscoveryForm formActionPath={providerAdminServiceDiscoveryServicesPath} setLoadingProjects={setLoadingProjects} />

return (
<React.Fragment>
Expand All @@ -35,6 +36,7 @@ const NewServiceForm = (props: Props) => {
isServiceDiscoveryUsable={isServiceDiscoveryUsable}
serviceDiscoveryAuthenticateUrl={serviceDiscoveryAuthenticateUrl}
handleFormsVisibility={handleFormsVisibility}
loadingProjects={loadingProjects}
/>
}
{formToRender()}
Expand Down
40 changes: 18 additions & 22 deletions app/javascript/src/NewService/components/ServiceDiscoveryForm.jsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
// @flow

import React from 'react'
import {useState, useEffect} from 'react'
import React, {useEffect} from 'react'

import {FormWrapper, ErrorMessage,
ServiceDiscoveryListItems} from 'NewService/components/FormElements'
import {fetchData} from 'utilities/utils'
import type {FormProps} from 'NewService/types'

const BASE_PATH = '/p/admin/service_discovery'
const PROJECTS_PATH = `${BASE_PATH}/projects.json`
import {PROJECTS_PATH} from 'NewService'

const ServiceDiscoveryForm = ({formActionPath}: {formActionPath: string}) => {
const [projects, setProjects] = useState([])
const [services, setServices] = useState([])
const [fetchErrorMessage, setFetchErrorMessage] = useState('')
type Props = {
formActionPath: string,
setLoadingProjects: boolean => void
}

const ServiceDiscoveryForm = ({formActionPath, setLoadingProjects}: Props) => {
// Don't use named imports so that useState can be mocked in specs
josemigallas marked this conversation as resolved.
Show resolved Hide resolved
const [projects, setProjects] = React.useState([])
const [fetchErrorMessage, setFetchErrorMessage] = React.useState('')

const fetchProjects = async () => {
try {
const data = await fetchData(PROJECTS_PATH)
setProjects(data['projects'])
fetchServices(data.projects[0].metadata.name)
} catch (error) {
setFetchErrorMessage(error.message)
}
}
setLoadingProjects(true)

const fetchServices = async (namespace: string) => {
try {
const data = await fetchData(`${BASE_PATH}/namespaces/${namespace}/services.json`)
setServices(data['services'])
const projects = await fetchData<string[]>(PROJECTS_PATH)
setProjects(projects)
} catch (error) {
setFetchErrorMessage(error.message)
} finally {
setLoadingProjects(false)
}
}

const listItemsProps = {fetchServices, projects, services}
const listItemsProps = {projects, onError: setFetchErrorMessage}

useEffect(() => {
fetchProjects()
}, [])

const formProps: FormProps = {
const formProps = {
id: 'service_source',
formActionPath,
hasHiddenServiceDiscoveryInput: true,
Expand Down
30 changes: 23 additions & 7 deletions app/javascript/src/NewService/components/ServiceSourceForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,45 @@ import React from 'react'
type Props = {
isServiceDiscoveryUsable: boolean,
serviceDiscoveryAuthenticateUrl: string,
handleFormsVisibility: (event: SyntheticEvent<HTMLInputElement>) => void
handleFormsVisibility: (event: SyntheticEvent<HTMLInputElement>) => void,
loadingProjects: boolean
}

const ServiceSourceForm = (props: Props) => {
const {isServiceDiscoveryUsable, serviceDiscoveryAuthenticateUrl,
handleFormsVisibility} = props
handleFormsVisibility, loadingProjects} = props
const classNameDisabled = isServiceDiscoveryUsable ? '' : 'disabled'
return (
<form className="formtastic" id="new_service_source">
<fieldset className="inputs">
<ol>
<li className="radio">
<label htmlFor="source_manual">
<input type="radio" name="source" id="source_manual" value="manual" defaultChecked="defaultChecked" onChange={handleFormsVisibility}/>
<input
type="radio"
name="source"
id="source_manual"
value="manual"
disabled={loadingProjects}
defaultChecked="defaultChecked"
onChange={handleFormsVisibility}
/>
<span className="new-service-source-input">Define manually</span>
</label>
</li>
<li className="radio">
<label htmlFor="source_discover"
className={classNameDisabled}>
<input type="radio" name="source" id="source_discover" value="discover" disabled={!isServiceDiscoveryUsable} onChange={handleFormsVisibility}/>
<label htmlFor="source_discover" className={classNameDisabled}>
<input
type="radio"
name="source"
id="source_discover"
value="discover"
disabled={!isServiceDiscoveryUsable || loadingProjects}
onChange={handleFormsVisibility}
/>
<span className="new-service-source-input">Import from OpenShift</span>
{ isServiceDiscoveryUsable ||
{loadingProjects && <i className="fa fa-spinner fa-spin" />}
{isServiceDiscoveryUsable ||
<a href={serviceDiscoveryAuthenticateUrl}>
{' (Authenticate to enable this option)'}
</a>
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/src/NewService/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// @flow

export {NewServiceForm, NewServiceFormWrapper} from 'NewService/components/NewServiceForm'
export {ServiceSourceForm} from 'NewService/components/ServiceSourceForm'
export {ServiceDiscoveryForm} from 'NewService/components/ServiceDiscoveryForm'
export {ServiceManualForm} from 'NewService/components/ServiceManualForm'

export const BASE_PATH = '/p/admin/service_discovery'
export const PROJECTS_PATH = `${BASE_PATH}/projects.json`
17 changes: 16 additions & 1 deletion app/javascript/src/Types/libs/libdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ declare var module: {

// TODO: remove these module declarations when not failing
declare module 'whatwg-fetch' {
declare module.exports: fetch
declare type Options = {
method?: string,
body?: string,
headers?: Object,
credentials?: 'omit' | 'same-origin' | 'include'
}
declare type Response = {
status: number,
statusText: string,
ok: boolean,
headers: any,
url: string,
text: () => Promise<string>,
json: () => Promise<Object>
}
declare export function fetch (url: string, options: ?Options): Promise<Response>
}

declare module 'core-js/fn/symbol' {
Expand Down