diff --git a/packages/ui/src/components/base/TimeFramePicker.vue b/packages/ui/src/components/base/TimeFramePicker.vue
new file mode 100644
index 0000000000..38d6b1726d
--- /dev/null
+++ b/packages/ui/src/components/base/TimeFramePicker.vue
@@ -0,0 +1,946 @@
+
+
+
+
+
+
+
+
+
+
+ {{ rangeLabel }}:
+ {{ formattedRange }}
+
+
+
+
+
+ {{ formatMessage(messages.emptyRange) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.lastTimeframePrefix) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index 5dfe375f43..ca1e6e95db 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -4301,6 +4301,87 @@
"tag.loader.waterfall": {
"defaultMessage": "Waterfall"
},
+ "time-frame-picker.apply": {
+ "defaultMessage": "Apply"
+ },
+ "time-frame-picker.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "time-frame-picker.clear-range": {
+ "defaultMessage": "Clear"
+ },
+ "time-frame-picker.custom-range": {
+ "defaultMessage": "Custom fixed date range..."
+ },
+ "time-frame-picker.decrease-amount": {
+ "defaultMessage": "Decrease timeframe amount"
+ },
+ "time-frame-picker.empty-range": {
+ "defaultMessage": "No date range selected."
+ },
+ "time-frame-picker.increase-amount": {
+ "defaultMessage": "Increase timeframe amount"
+ },
+ "time-frame-picker.last-timeframe": {
+ "defaultMessage": "In the last {amount} {unit, select, hours {{amount, plural, one {hour} other {hours}}} days {{amount, plural, one {day} other {days}}} weeks {{amount, plural, one {week} other {weeks}}} months {{amount, plural, one {month} other {months}}} other {days}}"
+ },
+ "time-frame-picker.last-timeframe-prefix": {
+ "defaultMessage": "In the last"
+ },
+ "time-frame-picker.option.all-time": {
+ "defaultMessage": "All time"
+ },
+ "time-frame-picker.option.last-14-days": {
+ "defaultMessage": "Last 14 days"
+ },
+ "time-frame-picker.option.last-180-days": {
+ "defaultMessage": "Last 180 days"
+ },
+ "time-frame-picker.option.last-30-days": {
+ "defaultMessage": "Last 30 days"
+ },
+ "time-frame-picker.option.last-7-days": {
+ "defaultMessage": "Last 7 days"
+ },
+ "time-frame-picker.option.last-90-days": {
+ "defaultMessage": "Last 90 days"
+ },
+ "time-frame-picker.option.today": {
+ "defaultMessage": "Today"
+ },
+ "time-frame-picker.option.year-to-date": {
+ "defaultMessage": "Year to date"
+ },
+ "time-frame-picker.option.yesterday": {
+ "defaultMessage": "Yesterday"
+ },
+ "time-frame-picker.select-timeframe": {
+ "defaultMessage": "Select timeframe"
+ },
+ "time-frame-picker.selected-range": {
+ "defaultMessage": "Selected"
+ },
+ "time-frame-picker.selecting-range": {
+ "defaultMessage": "Selecting"
+ },
+ "time-frame-picker.timeframe-amount": {
+ "defaultMessage": "Timeframe amount"
+ },
+ "time-frame-picker.timeframe-unit": {
+ "defaultMessage": "Timeframe unit"
+ },
+ "time-frame-picker.unit.days": {
+ "defaultMessage": "days"
+ },
+ "time-frame-picker.unit.hours": {
+ "defaultMessage": "hours"
+ },
+ "time-frame-picker.unit.months": {
+ "defaultMessage": "months"
+ },
+ "time-frame-picker.unit.weeks": {
+ "defaultMessage": "weeks"
+ },
"ui.component.unsaved-changes-popup.body": {
"defaultMessage": "You have unsaved changes."
},
diff --git a/packages/ui/src/stories/base/TimeFramePicker.stories.ts b/packages/ui/src/stories/base/TimeFramePicker.stories.ts
new file mode 100644
index 0000000000..7c511faf3b
--- /dev/null
+++ b/packages/ui/src/stories/base/TimeFramePicker.stories.ts
@@ -0,0 +1,86 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+import { ref } from 'vue'
+
+import TimeFramePicker, {
+ type TimeFrameLastUnit,
+ type TimeFrameMode,
+ type TimeFramePreset,
+} from '../../components/base/TimeFramePicker.vue'
+
+const meta = {
+ title: 'Base/TimeFramePicker',
+ component: TimeFramePicker,
+ parameters: {
+ layout: 'padded',
+ },
+ decorators: [
+ (story) => ({
+ components: { story },
+ template: '
',
+ }),
+ ],
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+function renderPicker(initial: {
+ mode?: TimeFrameMode
+ preset?: TimeFramePreset
+ lastAmount?: number
+ lastUnit?: TimeFrameLastUnit
+ customStartDate?: string
+ customEndDate?: string
+} = {}) {
+ return () => ({
+ components: { TimeFramePicker },
+ setup() {
+ const mode = ref(initial.mode ?? 'preset')
+ const preset = ref(initial.preset ?? 'last_30_days')
+ const lastAmount = ref(initial.lastAmount ?? 1)
+ const lastUnit = ref(initial.lastUnit ?? 'days')
+ const customStartDate = ref(initial.customStartDate ?? '2026-04-23')
+ const customEndDate = ref(initial.customEndDate ?? '2026-05-22')
+
+ return {
+ customEndDate,
+ customStartDate,
+ lastAmount,
+ lastUnit,
+ mode,
+ preset,
+ }
+ },
+ template: /* html */ `
+
+ `,
+ })
+}
+
+export const Preset: Story = {
+ render: renderPicker(),
+}
+
+export const LastTimeframe: Story = {
+ render: renderPicker({
+ mode: 'last',
+ lastAmount: 12,
+ lastUnit: 'hours',
+ }),
+}
+
+export const CustomRange: Story = {
+ render: renderPicker({
+ mode: 'custom_range',
+ customStartDate: '2026-04-23',
+ customEndDate: '2026-05-22',
+ }),
+}