http.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  4. import ActionButton from 'sentry/components/actions/button';
  5. import Button from 'sentry/components/button';
  6. import Field from 'sentry/components/forms/field';
  7. import SelectField from 'sentry/components/forms/selectField';
  8. import Input from 'sentry/components/input';
  9. import {
  10. DEBUG_SOURCE_CASINGS,
  11. DEBUG_SOURCE_LAYOUTS,
  12. DEBUG_SOURCE_TYPES,
  13. } from 'sentry/data/debugFileSources';
  14. import {IconClose} from 'sentry/icons/iconClose';
  15. import {t, tct} from 'sentry/locale';
  16. import {INPUT_PADDING} from 'sentry/styles/input';
  17. import space from 'sentry/styles/space';
  18. import {uniqueId} from 'sentry/utils/guid';
  19. const CLEAR_PASSWORD_BUTTON_SIZE = 22;
  20. const PASSWORD_INPUT_PADDING_RIGHT = INPUT_PADDING + CLEAR_PASSWORD_BUTTON_SIZE;
  21. type InitialData = {
  22. id: string;
  23. layout: {
  24. casing: keyof typeof DEBUG_SOURCE_CASINGS;
  25. type: keyof typeof DEBUG_SOURCE_LAYOUTS;
  26. };
  27. name: string;
  28. url: string;
  29. password?: {
  30. 'hidden-secret': boolean;
  31. };
  32. username?: string;
  33. };
  34. type Data = Partial<Pick<InitialData, 'name' | 'url'>> &
  35. Omit<InitialData, 'name' | 'url' | 'password' | 'layout'> & {
  36. 'layout.casing': keyof typeof DEBUG_SOURCE_CASINGS;
  37. 'layout.type': keyof typeof DEBUG_SOURCE_LAYOUTS;
  38. password?: string;
  39. };
  40. type SubmitData = Omit<Data, 'password' | 'name' | 'url'> &
  41. Pick<InitialData, 'name' | 'url'> & {
  42. password?:
  43. | {
  44. 'hidden-secret': boolean;
  45. }
  46. | string;
  47. };
  48. type Props = Pick<ModalRenderProps, 'Header' | 'Body' | 'Footer'> & {
  49. onSubmit: (data: SubmitData) => void;
  50. initialData?: InitialData;
  51. };
  52. function Http({Header, Body, Footer, onSubmit, ...props}: Props) {
  53. const initialData: Data = {
  54. id: props.initialData?.id ?? uniqueId(),
  55. name: props.initialData?.name,
  56. url: props.initialData?.url,
  57. username: props.initialData?.username,
  58. password: typeof props.initialData?.password === 'object' ? undefined : '',
  59. 'layout.type': props.initialData?.layout.type ?? 'native',
  60. 'layout.casing': props.initialData?.layout.casing ?? 'default',
  61. };
  62. const [data, setData] = useState<Data>(initialData);
  63. function isFormInvalid() {
  64. return !data.name || !data.url;
  65. }
  66. function formUnchanged() {
  67. return data === initialData;
  68. }
  69. function handleSubmit() {
  70. const validData = data as SubmitData;
  71. onSubmit({
  72. id: validData.id,
  73. name: validData.name,
  74. url: validData.url,
  75. 'layout.type': validData['layout.type'],
  76. 'layout.casing': validData['layout.casing'],
  77. username: validData.username,
  78. password:
  79. validData.password === undefined
  80. ? {'hidden-secret': true}
  81. : !validData.password
  82. ? undefined
  83. : validData.password,
  84. });
  85. }
  86. function handleClearPassword() {
  87. setData({...data, password: ''});
  88. }
  89. return (
  90. <Fragment>
  91. <Header closeButton>
  92. {initialData
  93. ? tct('Update [name] Repository', {name: DEBUG_SOURCE_TYPES.http})
  94. : tct('Add [name] Repository', {name: DEBUG_SOURCE_TYPES.http})}
  95. </Header>
  96. <Body>
  97. <Field
  98. label={t('Name')}
  99. inline={false}
  100. help={t('A display name for this repository')}
  101. flexibleControlStateSize
  102. stacked
  103. required
  104. >
  105. <Input
  106. type="text"
  107. name="name"
  108. placeholder={t('New Repository')}
  109. value={data.name}
  110. onChange={e =>
  111. setData({
  112. ...data,
  113. name: e.target.value,
  114. })
  115. }
  116. />
  117. </Field>
  118. <hr />
  119. <Field
  120. label={t('Download Url')}
  121. inline={false}
  122. help={t('Full URL to the symbol server')}
  123. flexibleControlStateSize
  124. stacked
  125. required
  126. >
  127. <Input
  128. type="text"
  129. name="url"
  130. placeholder="https://msdl.microsoft.com/download/symbols/"
  131. value={data.url}
  132. onChange={e =>
  133. setData({
  134. ...data,
  135. url: e.target.value,
  136. })
  137. }
  138. />
  139. </Field>
  140. <Field
  141. label={t('User')}
  142. inline={false}
  143. help={t('User for HTTP basic auth')}
  144. flexibleControlStateSize
  145. stacked
  146. >
  147. <Input
  148. type="text"
  149. name="username"
  150. placeholder="admin"
  151. value={data.username}
  152. onChange={e =>
  153. setData({
  154. ...data,
  155. username: e.target.value,
  156. })
  157. }
  158. />
  159. </Field>
  160. <Field
  161. label={t('Password')}
  162. inline={false}
  163. help={t('Password for HTTP basic auth')}
  164. flexibleControlStateSize
  165. stacked
  166. >
  167. <PasswordInput
  168. type={data.password === undefined ? 'text' : 'password'}
  169. name="url"
  170. placeholder={
  171. data.password === undefined ? t('(Password unchanged)') : 'open-sesame'
  172. }
  173. value={data.password}
  174. onChange={e =>
  175. setData({
  176. ...data,
  177. password: e.target.value,
  178. })
  179. }
  180. />
  181. {(data.password === undefined ||
  182. (typeof data.password === 'string' && !!data.password)) && (
  183. <ClearPasswordButton
  184. onClick={handleClearPassword}
  185. icon={<IconClose size="14px" />}
  186. size="xs"
  187. title={t('Clear password')}
  188. aria-label={t('Clear password')}
  189. borderless
  190. />
  191. )}
  192. </Field>
  193. <hr />
  194. <StyledSelectField
  195. name="layout.type"
  196. label={t('Directory Layout')}
  197. help={t('The layout of the folder structure.')}
  198. options={Object.keys(DEBUG_SOURCE_LAYOUTS).map(key => ({
  199. value: key,
  200. label: DEBUG_SOURCE_LAYOUTS[key],
  201. }))}
  202. value={data['layout.type']}
  203. onChange={value =>
  204. setData({
  205. ...data,
  206. ['layout.type']: value,
  207. })
  208. }
  209. inline={false}
  210. flexibleControlStateSize
  211. stacked
  212. />
  213. <StyledSelectField
  214. name="layout.casing"
  215. label={t('Path Casing')}
  216. help={t('The case of files and folders.')}
  217. options={Object.keys(DEBUG_SOURCE_CASINGS).map(key => ({
  218. value: key,
  219. label: DEBUG_SOURCE_CASINGS[key],
  220. }))}
  221. value={data['layout.casing']}
  222. onChange={value =>
  223. setData({
  224. ...data,
  225. ['layout.casing']: value,
  226. })
  227. }
  228. inline={false}
  229. flexibleControlStateSize
  230. stacked
  231. />
  232. </Body>
  233. <Footer>
  234. <Button
  235. onClick={handleSubmit}
  236. priority="primary"
  237. disabled={isFormInvalid() || formUnchanged()}
  238. >
  239. {t('Save changes')}
  240. </Button>
  241. </Footer>
  242. </Fragment>
  243. );
  244. }
  245. export default Http;
  246. const StyledSelectField = styled(SelectField)`
  247. padding-right: 0;
  248. `;
  249. const PasswordInput = styled(Input)`
  250. padding-right: ${PASSWORD_INPUT_PADDING_RIGHT}px;
  251. `;
  252. const ClearPasswordButton = styled(ActionButton)`
  253. background: transparent;
  254. height: ${CLEAR_PASSWORD_BUTTON_SIZE}px;
  255. width: ${CLEAR_PASSWORD_BUTTON_SIZE}px;
  256. padding: 0;
  257. position: absolute;
  258. top: 50%;
  259. right: ${space(0.75)};
  260. transform: translateY(-50%);
  261. svg {
  262. color: ${p => p.theme.gray400};
  263. :hover {
  264. color: hsl(0, 0%, 60%);
  265. }
  266. }
  267. `;