{"version":3,"file":"RelativeTimeRangePicker.mjs","sources":["../../../../../src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx"],"sourcesContent":["import { css, cx } from '@emotion/css';\nimport { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react';\nimport { useDialog } from '@react-aria/dialog';\nimport { FocusScope } from '@react-aria/focus';\nimport { useOverlay } from '@react-aria/overlays';\nimport { FormEvent, useCallback, useRef, useState } from 'react';\n\nimport { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data';\n\nimport { useStyles2 } from '../../../themes';\nimport { Trans, t } from '../../../utils/i18n';\nimport { Button } from '../../Button';\nimport { Field } from '../../Forms/Field';\nimport { Icon } from '../../Icon/Icon';\nimport { getInputStyles, Input } from '../../Input/Input';\nimport { ScrollContainer } from '../../ScrollContainer/ScrollContainer';\nimport { Tooltip } from '../../Tooltip/Tooltip';\nimport { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle';\nimport { TimeRangeList } from '../TimeRangePicker/TimeRangeList';\nimport { quickOptions } from '../options';\n\nimport {\n  isRangeValid,\n  isRelativeFormat,\n  mapOptionToRelativeTimeRange,\n  mapRelativeTimeRangeToOption,\n  RangeValidation,\n} from './utils';\n\n/**\n * @internal\n */\nexport interface RelativeTimeRangePickerProps {\n  timeRange: RelativeTimeRange;\n  onChange: (timeRange: RelativeTimeRange) => void;\n}\n\ntype InputState = {\n  value: string;\n  validation: RangeValidation;\n};\n\nconst validOptions = quickOptions.filter((o) => isRelativeFormat(o.from));\n\n/**\n * @internal\n */\nexport function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) {\n  const { timeRange, onChange } = props;\n  const [isOpen, setIsOpen] = useState(false);\n  const onClose = useCallback(() => setIsOpen(false), []);\n  const timeOption = mapRelativeTimeRangeToOption(timeRange);\n  const [from, setFrom] = useState<InputState>({ value: timeOption.from, validation: isRangeValid(timeOption.from) });\n  const [to, setTo] = useState<InputState>({ value: timeOption.to, validation: isRangeValid(timeOption.to) });\n  const ref = useRef<HTMLDivElement>(null);\n  const { overlayProps, underlayProps } = useOverlay(\n    { onClose: () => setIsOpen(false), isDismissable: true, isOpen },\n    ref\n  );\n  const { dialogProps } = useDialog({}, ref);\n\n  // the order of middleware is important!\n  // see https://floating-ui.com/docs/arrow#order\n  const middleware = [\n    flip({\n      // see https://floating-ui.com/docs/flip#combining-with-shift\n      crossAxis: false,\n      boundary: document.body,\n    }),\n    shift(),\n  ];\n\n  const { context, refs, floatingStyles } = useFloating({\n    open: isOpen,\n    placement: 'bottom-start',\n    onOpenChange: setIsOpen,\n    middleware,\n    whileElementsMounted: autoUpdate,\n    strategy: 'fixed',\n  });\n\n  const click = useClick(context);\n  const dismiss = useDismiss(context);\n\n  const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);\n\n  const styles = useStyles2(getStyles(from.validation.errorMessage, to.validation.errorMessage));\n\n  const onChangeTimeOption = (option: TimeOption) => {\n    const relativeTimeRange = mapOptionToRelativeTimeRange(option);\n    if (!relativeTimeRange) {\n      return;\n    }\n    onClose();\n    setFrom({ ...from, value: option.from });\n    setTo({ ...to, value: option.to });\n    onChange(relativeTimeRange);\n  };\n\n  const onOpen = useCallback(\n    (event: FormEvent<HTMLButtonElement>) => {\n      event.stopPropagation();\n      event.preventDefault();\n      setIsOpen(!isOpen);\n    },\n    [isOpen]\n  );\n\n  const onApply = (event: FormEvent<HTMLButtonElement>) => {\n    event.preventDefault();\n\n    if (!to.validation.isValid || !from.validation.isValid) {\n      return;\n    }\n\n    const timeRange = mapOptionToRelativeTimeRange({\n      from: from.value,\n      to: to.value,\n      display: '',\n    });\n\n    if (!timeRange) {\n      return;\n    }\n\n    onChange(timeRange);\n    setIsOpen(false);\n  };\n\n  const { from: timeOptionFrom, to: timeOptionTo } = timeOption;\n\n  return (\n    <div className={styles.container}>\n      <button\n        ref={refs.setReference}\n        className={styles.pickerInput}\n        type=\"button\"\n        onClick={onOpen}\n        {...getReferenceProps()}\n      >\n        <span className={styles.clockIcon}>\n          <Icon name=\"clock-nine\" />\n        </span>\n        <span>\n          <Trans i18nKey=\"time-picker.time-range.from-to\">\n            {{ timeOptionFrom }} to {{ timeOptionTo }}\n          </Trans>\n        </span>\n        <span className={styles.caretIcon}>\n          <Icon name={isOpen ? 'angle-up' : 'angle-down'} size=\"lg\" />\n        </span>\n      </button>\n      {isOpen && (\n        <div>\n          <div role=\"presentation\" className={styles.backdrop} {...underlayProps} />\n          <FocusScope contain autoFocus restoreFocus>\n            <div ref={ref} {...overlayProps} {...dialogProps}>\n              <div className={styles.content} ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>\n                <div className={styles.body}>\n                  <div className={styles.leftSide}>\n                    <ScrollContainer showScrollIndicators>\n                      <TimeRangeList\n                        title={t('time-picker.time-range.example-title', 'Example time ranges')}\n                        options={validOptions}\n                        onChange={onChangeTimeOption}\n                        value={timeOption}\n                      />\n                    </ScrollContainer>\n                  </div>\n                  <div className={styles.rightSide}>\n                    <div className={styles.title}>\n                      <TimePickerTitle>\n                        <Tooltip content={<TooltipContent />} placement=\"bottom\" theme=\"info\">\n                          <div>\n                            <Trans i18nKey=\"time-picker.time-range.specify\">\n                              Specify time range <Icon name=\"info-circle\" />\n                            </Trans>\n                          </div>\n                        </Tooltip>\n                      </TimePickerTitle>\n                    </div>\n                    <Field\n                      label={t('time-picker.time-range.from-label', 'From')}\n                      invalid={!from.validation.isValid}\n                      error={from.validation.errorMessage}\n                    >\n                      <Input\n                        onClick={(event) => event.stopPropagation()}\n                        onBlur={() => setFrom({ ...from, validation: isRangeValid(from.value) })}\n                        onChange={(event) => setFrom({ ...from, value: event.currentTarget.value })}\n                        value={from.value}\n                      />\n                    </Field>\n                    <Field\n                      label={t('time-picker.time-range.to-label', 'To')}\n                      invalid={!to.validation.isValid}\n                      error={to.validation.errorMessage}\n                    >\n                      <Input\n                        onClick={(event) => event.stopPropagation()}\n                        onBlur={() => setTo({ ...to, validation: isRangeValid(to.value) })}\n                        onChange={(event) => setTo({ ...to, value: event.currentTarget.value })}\n                        value={to.value}\n                      />\n                    </Field>\n                    <Button\n                      aria-label={t('time-picker.time-range.submit-button-label', 'TimePicker submit button')}\n                      onClick={onApply}\n                    >\n                      <Trans i18nKey=\"time-picker.time-range.apply\">Apply time range</Trans>\n                    </Button>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </FocusScope>\n        </div>\n      )}\n    </div>\n  );\n}\n\nconst TooltipContent = () => {\n  const styles = useStyles2(toolTipStyles);\n  return (\n    <>\n      <div className={styles.supported}>\n        <Trans i18nKey=\"time-picker.time-range.supported-formats\">\n          Supported formats: <code className={styles.tooltip}>now-[digit]s/m/h/d/w</code>\n        </Trans>\n      </div>\n      <div>\n        <Trans i18nKey=\"time-picker.time-range.example\">\n          Example: to select a time range from 10 minutes ago to now\n        </Trans>\n      </div>\n      <code className={styles.tooltip}>\n        <Trans i18nKey=\"time-picker.time-range.example-details\">From: now-10m To: now</Trans>\n      </code>\n      <div className={styles.link}>\n        <Trans i18nKey=\"time-picker.time-range.more-info\">\n          For more information see{' '}\n          <a href=\"https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/\">\n            docs <Icon name=\"external-link-alt\" />\n          </a>\n          .\n        </Trans>\n      </div>\n    </>\n  );\n};\n\nconst toolTipStyles = (theme: GrafanaTheme2) => ({\n  supported: css({\n    marginBottom: theme.spacing(1),\n  }),\n  tooltip: css({\n    margin: 0,\n  }),\n  link: css({\n    marginTop: theme.spacing(1),\n  }),\n});\n\nconst getStyles = (fromError?: string, toError?: string) => (theme: GrafanaTheme2) => {\n  const inputStyles = getInputStyles({ theme, invalid: false });\n  const bodyMinimumHeight = 250;\n  const bodyHeight = bodyMinimumHeight + calculateErrorHeight(theme, fromError) + calculateErrorHeight(theme, toError);\n\n  return {\n    backdrop: css({\n      position: 'fixed',\n      zIndex: theme.zIndex.modalBackdrop,\n      top: 0,\n      right: 0,\n      bottom: 0,\n      left: 0,\n    }),\n    container: css({\n      display: 'flex',\n      position: 'relative',\n    }),\n    pickerInput: cx(\n      inputStyles.input,\n      inputStyles.wrapper,\n      css({\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'space-between',\n        cursor: 'pointer',\n        paddingRight: 0,\n        paddingLeft: 0,\n        lineHeight: `${theme.spacing.gridSize * theme.components.height.md - 2}px`,\n      })\n    ),\n    caretIcon: cx(\n      inputStyles.suffix,\n      css({\n        position: 'relative',\n        marginLeft: theme.spacing(0.5),\n      })\n    ),\n    clockIcon: cx(\n      inputStyles.prefix,\n      css({\n        position: 'relative',\n        marginRight: theme.spacing(0.5),\n      })\n    ),\n    content: css({\n      background: theme.colors.background.primary,\n      boxShadow: theme.shadows.z3,\n      position: 'absolute',\n      zIndex: theme.zIndex.modal,\n      width: '500px',\n      top: '100%',\n      borderRadius: theme.shape.radius.default,\n      border: `1px solid ${theme.colors.border.weak}`,\n      left: 0,\n      whiteSpace: 'normal',\n    }),\n    body: css({\n      display: 'flex',\n      height: `${bodyHeight}px`,\n    }),\n    description: css({\n      color: theme.colors.text.secondary,\n      fontSize: theme.typography.size.sm,\n    }),\n    leftSide: css({\n      width: '50% !important',\n      borderRight: `1px solid ${theme.colors.border.medium}`,\n    }),\n    rightSide: css({\n      width: '50%',\n      padding: theme.spacing(1),\n    }),\n    title: css({\n      marginBottom: theme.spacing(1),\n    }),\n  };\n};\n\nfunction calculateErrorHeight(theme: GrafanaTheme2, errorMessage?: string): number {\n  if (!errorMessage) {\n    return 0;\n  }\n\n  if (errorMessage.length > 34) {\n    return theme.spacing.gridSize * 6.5;\n  }\n\n  return theme.spacing.gridSize * 4;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0CqB,aAAa,MAAO,CAAA,CAAC,MAAM,gBAAiB,CAAA,CAAA,CAAE,IAAI,CAAC"}