PropertiesTable.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled, { css } from 'styled-components'
  17. import Switch from '@src/components/ui/Switch'
  18. import TextInput from '@src/components/ui/TextInput'
  19. import LabelDictionary from '@src/utils/LabelDictionary'
  20. import { ThemePalette, ThemeProps } from '@src/components/Theme'
  21. import Dropdown from '@src/components/ui/Dropdowns/Dropdown'
  22. import AutocompleteDropdown from '@src/components/ui/Dropdowns/AutocompleteDropdown'
  23. import { Field, EnumItem, isEnumSeparator } from '@src/@types/Field'
  24. const Wrapper = styled.div<{
  25. width?: number,
  26. highlight?: boolean,
  27. disabled?: boolean,
  28. disabledLoading?: boolean
  29. }>`
  30. display: flex;
  31. ${props => (props.width ? `width: ${props.width - 2}px;` : '')}
  32. flex-direction: column;
  33. border: 1px solid ${props => (props.highlight ? ThemePalette.alert : ThemePalette.grayscale[2])};
  34. border-radius: ${ThemeProps.borderRadius};
  35. ${props => (props.disabled ? css`
  36. opacity: 0.5;
  37. ` : '')}
  38. ${props => (props.disabledLoading ? ThemeProps.animations.disabledLoading : '')}
  39. `
  40. const Column = styled.div<any>`
  41. ${ThemeProps.exactWidth('calc(50% - 24px)')}
  42. height: 32px;
  43. padding: 0 8px 0 16px;
  44. display: flex;
  45. align-items: center;
  46. > span {
  47. white-space: nowrap;
  48. overflow: hidden;
  49. text-overflow: ellipsis;
  50. min-width: 0;
  51. }
  52. ${props => (props.header ? css`
  53. color: ${ThemePalette.grayscale[4]};
  54. background: ${ThemePalette.grayscale[7]};
  55. ` : '')}
  56. `
  57. const Row = styled.div<any>`
  58. display: flex;
  59. align-items: center;
  60. border-bottom: 1px solid ${ThemePalette.grayscale[2]};
  61. &:last-child {
  62. border-bottom: 0;
  63. }
  64. &:first-child ${Column} {
  65. border-top-left-radius: ${ThemeProps.borderRadius};
  66. }
  67. &:last-child ${Column} {
  68. border-bottom-left-radius: ${ThemeProps.borderRadius};
  69. }
  70. `
  71. type Props = {
  72. properties: Field[],
  73. highlight?: boolean,
  74. onChange: (property: Field, value: any) => void,
  75. valueCallback: (property: Field) => any,
  76. hideRequiredSymbol?: boolean,
  77. disabled?: boolean,
  78. disabledLoading?: boolean,
  79. labelRenderer?: ((propName: string) => string) | null,
  80. width?: number,
  81. }
  82. @observer
  83. class PropertiesTable extends React.Component<Props> {
  84. getName(propName: string): string {
  85. if (this.props.labelRenderer) {
  86. return this.props.labelRenderer(propName)
  87. }
  88. if (propName.indexOf('/') > -1) {
  89. return LabelDictionary.get(propName.substr(propName.lastIndexOf('/') + 1))
  90. }
  91. return LabelDictionary.get(propName)
  92. }
  93. renderSwitch(prop: Field, opts: { triState: boolean }) {
  94. return (
  95. <Switch
  96. secondary
  97. disabled={this.props.disabledLoading}
  98. triState={opts.triState}
  99. height={16}
  100. checked={this.props.valueCallback(prop)}
  101. onChange={checked => { this.props.onChange(prop, checked) }}
  102. />
  103. )
  104. }
  105. renderTextInput(prop: Field) {
  106. return (
  107. <TextInput
  108. width="100%"
  109. embedded
  110. type={prop.password ? 'password' : 'text'}
  111. value={this.props.valueCallback(prop)}
  112. onChange={e => { this.props.onChange(prop, e.target.value) }}
  113. placeholder={this.getName(prop.name)}
  114. required={typeof prop.required === 'boolean' && !this.props.hideRequiredSymbol ? prop.required : false}
  115. disabled={this.props.disabledLoading}
  116. />
  117. )
  118. }
  119. renderEnumDropdown(prop: Field) {
  120. if (!prop.enum) {
  121. return null
  122. }
  123. let items = prop.enum.map((e: EnumItem) => {
  124. if (typeof e === 'string') {
  125. return {
  126. label: this.getName(e),
  127. value: e,
  128. }
  129. } if (isEnumSeparator(e)) {
  130. return { separator: true }
  131. }
  132. return {
  133. label: e.name,
  134. value: e.id,
  135. }
  136. })
  137. items = [
  138. { label: 'Choose a value', value: null },
  139. ...items,
  140. ]
  141. const selectedItem = items.find(i => !i.separator && i.value === this.props.valueCallback(prop))
  142. const commonProps = {
  143. embedded: true,
  144. width: 320,
  145. selectedItem,
  146. items,
  147. disabled: this.props.disabledLoading,
  148. onChange: (item: { value: any }) => this.props.onChange(prop, item.value),
  149. required: typeof prop.required === 'boolean' && !this.props.hideRequiredSymbol ? prop.required : false,
  150. }
  151. if (items.length < 10) {
  152. return (
  153. <Dropdown
  154. noSelectionMessage="Choose a value"
  155. dimFirstItem
  156. // eslint-disable-next-line react/jsx-props-no-spreading
  157. {...commonProps}
  158. />
  159. )
  160. }
  161. return (
  162. <AutocompleteDropdown
  163. dimNullValue
  164. // eslint-disable-next-line react/jsx-props-no-spreading
  165. {...commonProps}
  166. />
  167. )
  168. }
  169. renderInput(prop: Field) {
  170. let input = null
  171. switch (prop.type) {
  172. case 'boolean':
  173. input = this.renderSwitch(prop, { triState: Boolean(prop.nullableBoolean) })
  174. break
  175. case 'string':
  176. if (prop.enum) {
  177. input = this.renderEnumDropdown(prop)
  178. } else {
  179. input = this.renderTextInput(prop)
  180. }
  181. break
  182. default:
  183. }
  184. return input
  185. }
  186. render() {
  187. const hasRequiredInputs = this.props.properties.some(prop => prop.required && prop.type === 'string')
  188. const width = this.props.width && hasRequiredInputs ? this.props.width - 20 : this.props.width
  189. return (
  190. <Wrapper
  191. disabled={this.props.disabled}
  192. disabledLoading={this.props.disabledLoading}
  193. width={width}
  194. highlight={this.props.highlight}
  195. >
  196. {this.props.properties.map(prop => (
  197. <Row key={prop.name}>
  198. <Column header>
  199. <span title={this.getName(prop.label || prop.name)}>
  200. {this.getName(prop.label || prop.name)}
  201. </span>
  202. </Column>
  203. <Column input>{this.renderInput(prop)}</Column>
  204. </Row>
  205. ))}
  206. </Wrapper>
  207. )
  208. }
  209. }
  210. export default PropertiesTable