SSLCertificateField.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { IconShield } from "@tabler/icons-react";
  2. import { Field, useFormikContext } from "formik";
  3. import Select, { type ActionMeta, components, type OptionProps } from "react-select";
  4. import type { Certificate } from "src/api/backend";
  5. import { useCertificates } from "src/hooks";
  6. import { formatDateTime, intl, T } from "src/locale";
  7. interface CertOption {
  8. readonly value: number | "new";
  9. readonly label: string;
  10. readonly subLabel: string;
  11. readonly icon: React.ReactNode;
  12. }
  13. const Option = (props: OptionProps<CertOption>) => {
  14. return (
  15. <components.Option {...props}>
  16. <div className="flex-fill">
  17. <div className="font-weight-medium">
  18. {props.data.icon} <strong>{props.data.label}</strong>
  19. </div>
  20. <div className="text-secondary mt-1 ps-3">{props.data.subLabel}</div>
  21. </div>
  22. </components.Option>
  23. );
  24. };
  25. interface Props {
  26. id?: string;
  27. name?: string;
  28. label?: string;
  29. required?: boolean;
  30. allowNew?: boolean;
  31. forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields
  32. }
  33. export function SSLCertificateField({
  34. name = "certificateId",
  35. label = "ssl-certificate",
  36. id = "certificateId",
  37. required,
  38. allowNew,
  39. forHttp = true,
  40. }: Props) {
  41. const { isLoading, isError, error, data } = useCertificates();
  42. const { values, setFieldValue } = useFormikContext();
  43. const v: any = values || {};
  44. const handleChange = (newValue: any, _actionMeta: ActionMeta<CertOption>) => {
  45. setFieldValue(name, newValue?.value);
  46. const {
  47. sslForced,
  48. http2Support,
  49. hstsEnabled,
  50. hstsSubdomains,
  51. dnsChallenge,
  52. dnsProvider,
  53. dnsProviderCredentials,
  54. propagationSeconds,
  55. } = v;
  56. if (forHttp && !newValue?.value) {
  57. sslForced && setFieldValue("sslForced", false);
  58. http2Support && setFieldValue("http2Support", false);
  59. hstsEnabled && setFieldValue("hstsEnabled", false);
  60. hstsSubdomains && setFieldValue("hstsSubdomains", false);
  61. }
  62. if (newValue?.value !== "new") {
  63. dnsChallenge && setFieldValue("dnsChallenge", undefined);
  64. dnsProvider && setFieldValue("dnsProvider", undefined);
  65. dnsProviderCredentials && setFieldValue("dnsProviderCredentials", undefined);
  66. propagationSeconds && setFieldValue("propagationSeconds", undefined);
  67. }
  68. };
  69. const options: CertOption[] =
  70. data?.map((cert: Certificate) => ({
  71. value: cert.id,
  72. label: cert.niceName,
  73. subLabel: `${cert.provider === "letsencrypt" ? intl.formatMessage({ id: "lets-encrypt" }) : cert.provider} — ${intl.formatMessage({ id: "expires.on" }, { date: cert.expiresOn ? formatDateTime(cert.expiresOn) : "N/A" })}`,
  74. icon: <IconShield size={14} className="text-pink" />,
  75. })) || [];
  76. // Prepend the Add New option
  77. if (allowNew) {
  78. options?.unshift({
  79. value: "new",
  80. label: intl.formatMessage({ id: "certificates.request.title" }),
  81. subLabel: intl.formatMessage({ id: "certificates.request.subtitle" }),
  82. icon: <IconShield size={14} className="text-lime" />,
  83. });
  84. }
  85. // Prepend the None option
  86. if (!required) {
  87. options?.unshift({
  88. value: 0,
  89. label: intl.formatMessage({ id: "certificate.none.title" }),
  90. subLabel: forHttp
  91. ? intl.formatMessage({ id: "certificate.none.subtitle.for-http" })
  92. : intl.formatMessage({ id: "certificate.none.subtitle" }),
  93. icon: <IconShield size={14} className="text-red" />,
  94. });
  95. }
  96. return (
  97. <Field name={name}>
  98. {({ field, form }: any) => (
  99. <div className="mb-3">
  100. <label className="form-label" htmlFor={id}>
  101. <T id={label} />
  102. </label>
  103. {isLoading ? <div className="placeholder placeholder-lg col-12 my-3 placeholder-glow" /> : null}
  104. {isError ? <div className="invalid-feedback">{`${error}`}</div> : null}
  105. {!isLoading && !isError ? (
  106. <Select
  107. className="react-select-container"
  108. classNamePrefix="react-select"
  109. defaultValue={options.find((o) => o.value === field.value) || options[0]}
  110. options={options}
  111. components={{ Option }}
  112. styles={{
  113. option: (base) => ({
  114. ...base,
  115. height: "100%",
  116. }),
  117. }}
  118. onChange={handleChange}
  119. />
  120. ) : null}
  121. {form.errors[field.name] ? (
  122. <div className="invalid-feedback">
  123. {form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}
  124. </div>
  125. ) : null}
  126. </div>
  127. )}
  128. </Field>
  129. );
  130. }