settings.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import isEqual from 'lodash/isEqual';
  2. import Form from 'sentry/components/deprecatedforms/form';
  3. import FormState from 'sentry/components/forms/state';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import DefaultSettings from 'sentry/plugins/components/settings';
  6. type Field = Parameters<typeof DefaultSettings.prototype.renderField>[0]['config'];
  7. type FieldWithValues = Field & {defaultValue?: any; value?: any};
  8. type ApiData = {config: FieldWithValues[]; default_project?: string};
  9. type Props = DefaultSettings['props'];
  10. type State = DefaultSettings['state'] & {
  11. page: number;
  12. editing?: boolean;
  13. };
  14. const PAGE_FIELD_LIST = {
  15. 0: ['instance_url', 'username', 'password'],
  16. 1: ['default_project'],
  17. 2: ['ignored_fields', 'default_priority', 'default_issue_type', 'auto_create'],
  18. };
  19. class Settings extends DefaultSettings<Props, State> {
  20. constructor(props: Props, context: any) {
  21. super(props, context);
  22. Object.assign(this.state, {
  23. page: 0,
  24. });
  25. }
  26. isConfigured() {
  27. return !!this.state.formData?.default_project;
  28. }
  29. isLastPage = () => {
  30. return this.state.page === 2;
  31. };
  32. fetchData() {
  33. // This is mostly copy paste of parent class
  34. // except for setting edit state
  35. this.api.request(this.getPluginEndpoint(), {
  36. success: (data: ApiData) => {
  37. const formData: Record<string, any> = {};
  38. const initialData = {};
  39. data.config.forEach(field => {
  40. formData[field.name] = field.value || field.defaultValue;
  41. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  42. initialData[field.name] = field.value;
  43. });
  44. this.setState(
  45. {
  46. fieldList: data.config,
  47. formData,
  48. initialData,
  49. // start off in edit mode if there isn't a project set
  50. editing: !formData?.default_project,
  51. // call this here to prevent FormState.READY from being
  52. // set before fieldList is
  53. },
  54. this.onLoadSuccess
  55. );
  56. },
  57. error: this.onLoadError,
  58. });
  59. }
  60. startEditing = () => {
  61. this.setState({editing: true});
  62. };
  63. onSubmit() {
  64. if (isEqual(this.state.initialData, this.state.formData)) {
  65. if (this.isLastPage()) {
  66. this.setState({editing: false, page: 0});
  67. } else {
  68. this.setState({page: this.state.page + 1});
  69. }
  70. this.onSaveSuccess(this.onSaveComplete);
  71. return;
  72. }
  73. const body = Object.assign({}, this.state.formData);
  74. // if the project has changed, it's likely these values aren't valid anymore
  75. if (body.default_project !== this.state.initialData?.default_project) {
  76. body.default_issue_type = null;
  77. body.default_priority = null;
  78. }
  79. this.api.request(this.getPluginEndpoint(), {
  80. data: body,
  81. method: 'PUT',
  82. success: this.onSaveSuccess.bind(this, (data: ApiData) => {
  83. const formData = {};
  84. const initialData = {};
  85. data.config.forEach(field => {
  86. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  87. formData[field.name] = field.value || field.defaultValue;
  88. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  89. initialData[field.name] = field.value;
  90. });
  91. const state = {
  92. formData,
  93. initialData,
  94. errors: {},
  95. fieldList: data.config,
  96. page: this.state.page,
  97. editing: this.state.editing,
  98. };
  99. if (this.isLastPage()) {
  100. state.editing = false;
  101. state.page = 0;
  102. } else {
  103. state.page = this.state.page + 1;
  104. }
  105. this.setState(state);
  106. }),
  107. error: this.onSaveError.bind(this, (error: any) => {
  108. this.setState({
  109. errors: error.responseJSON?.errors || {},
  110. });
  111. }),
  112. complete: this.onSaveComplete,
  113. });
  114. }
  115. back = (ev: React.MouseEvent) => {
  116. ev.preventDefault();
  117. if (this.state.state === FormState.SAVING) {
  118. return;
  119. }
  120. this.setState({
  121. page: this.state.page - 1,
  122. });
  123. };
  124. render() {
  125. if (this.state.state === FormState.LOADING) {
  126. return <LoadingIndicator />;
  127. }
  128. if (this.state.state === FormState.ERROR && !this.state.fieldList) {
  129. return (
  130. <div className="alert alert-error m-b-1">
  131. An unknown error occurred. Need help with this?{' '}
  132. <a href="https://sentry.io/support/">Contact support</a>
  133. </div>
  134. );
  135. }
  136. const isSaving = this.state.state === FormState.SAVING;
  137. let fields: Field[] | undefined;
  138. let onSubmit: () => void;
  139. let submitLabel: string;
  140. if (this.state.editing) {
  141. fields = this.state.fieldList?.filter(f =>
  142. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  143. PAGE_FIELD_LIST[this.state.page].includes(f.name)
  144. );
  145. onSubmit = this.onSubmit;
  146. submitLabel = this.isLastPage() ? 'Finish' : 'Save and Continue';
  147. } else {
  148. fields = this.state.fieldList?.map(f => ({...f, readonly: true}));
  149. onSubmit = this.startEditing;
  150. submitLabel = 'Edit';
  151. }
  152. return (
  153. <Form
  154. onSubmit={onSubmit}
  155. submitDisabled={isSaving}
  156. submitLabel={submitLabel}
  157. extraButton={
  158. this.state.page === 0 ? null : (
  159. <a
  160. href="#"
  161. className={'btn btn-default pull-left' + (isSaving ? ' disabled' : '')}
  162. onClick={this.back}
  163. >
  164. Back
  165. </a>
  166. )
  167. }
  168. >
  169. {this.state.errors.__all__ && (
  170. <div className="alert alert-block alert-error">
  171. <ul>
  172. <li>{this.state.errors.__all__}</li>
  173. </ul>
  174. </div>
  175. )}
  176. {fields?.map(f =>
  177. this.renderField({
  178. config: f,
  179. formData: this.state.formData,
  180. formErrors: this.state.errors,
  181. onChange: this.changeField.bind(this, f.name),
  182. })
  183. )}
  184. </Form>
  185. );
  186. }
  187. }
  188. export default Settings;