Procházet zdrojové kódy

HUE-9485 [frontend] Add a date range picker dropdown Vue component

This adds a panel with preset ranges as well as custom range input
Johan Ahlen před 5 roky
rodič
revize
541d6597e5

+ 22 - 0
desktop/core/src/desktop/js/components/DateRangePicker.d.ts

@@ -0,0 +1,22 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export interface Range {
+  title: string;
+  from: number;
+  to: number;
+  custom?: boolean;
+}

+ 28 - 0
desktop/core/src/desktop/js/components/DateRangePicker.test.ts

@@ -0,0 +1,28 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { shallowMount } from '@vue/test-utils';
+import DropdownPanel from './dropdown/DropdownPanel.vue';
+import DateRangePicker from './DateRangePicker.vue';
+
+describe('DateRangePicker.vue', () => {
+  it('should render', () => {
+    const wrapper = shallowMount(DateRangePicker, {
+      stubs: { 'dropdown-panel': DropdownPanel }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 291 - 0
desktop/core/src/desktop/js/components/DateRangePicker.vue

@@ -0,0 +1,291 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<template>
+  <dropdown-panel :text="selectedRange.title" :inline="inline">
+    <template #contents="{ closePanel }">
+      <div class="date-range-picker-panel">
+        <div class="date-range-picker-body">
+          <div class="date-range-picker-preset">
+            <div style="border-right: 1px solid gray;">
+              <header>Quick Ranges</header>
+              <div class="preset-list">
+                <div v-for="(rangeSet, index) in rangeSets" :key="index" class="preset-column">
+                  <div
+                    v-for="range in rangeSet"
+                    :key="range.title"
+                    class="preset-value"
+                    :class="{ selected: range === selectedRange }"
+                  >
+                    <hue-link @click="selectedRange = range">{{ range.title }}</hue-link>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class="date-range-picker-custom">
+            <header>Time Range</header>
+            <div>
+              <div class="range-header">FROM:</div>
+              <datepicker
+                :value="customFrom"
+                :typeable="true"
+                input-class="range-input"
+                calendar-class="range-calendar"
+                format="dd/MM/yyyy"
+                placeholder="DD/MM/YYYY"
+                @selected="setCustomFrom"
+              />
+            </div>
+            <div>
+              <div class="range-header">TO:</div>
+              <datepicker
+                :value="customTo"
+                :typeable="true"
+                input-class="range-input"
+                calendar-class="range-calendar"
+                format="dd/MM/yyyy"
+                placeholder="DD/MM/YYYY"
+                @selected="setCustomTo"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="date-range-picker-footer">
+          <hue-button @click="closePanel">Cancel</hue-button>
+          <hue-button @click="apply(closePanel)">Apply</hue-button>
+        </div>
+      </div>
+    </template>
+  </dropdown-panel>
+</template>
+
+<script lang="ts">
+  import { DateTime } from 'luxon';
+  import Datepicker from 'vuejs-datepicker';
+  import HueLink from './HueLink.vue';
+  import HueButton from './HueButton.vue';
+  import I18n from '../utils/i18n';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import DropdownPanel from './dropdown/DropdownPanel.vue';
+
+  const SECOND = 1000;
+  const MINUTE = 60 * SECOND;
+  const HOUR = 60 * MINUTE;
+  const DAY = 24 * HOUR;
+  const WEEK = 7 * DAY;
+  const MONTH = (365 / 12) * DAY;
+  const YEAR = 365 * DAY;
+
+  const RANGE_NOW = DateTime.utc();
+  const TODAY_START = RANGE_NOW.valueOf() - RANGE_NOW.startOf('day').valueOf();
+  const WEEK_START = RANGE_NOW.valueOf() - RANGE_NOW.startOf('week').valueOf();
+  const MONTH_START = RANGE_NOW.valueOf() - RANGE_NOW.startOf('month').valueOf();
+  const YEAR_START = RANGE_NOW.valueOf() - RANGE_NOW.startOf('year').valueOf();
+
+  // Non-custom ranges have relative from/to, custom are absolute
+  // due to third-party datepicker component.
+  const RANGE_SETS: Range[][] = [
+    [
+      { title: I18n('Last 7 days'), from: 7 * DAY, to: 0 },
+      { title: I18n('Last 30 days'), from: 30 * DAY, to: 0 },
+      { title: I18n('Last 60 days'), from: 60 * DAY, to: 0 },
+      { title: I18n('Last 90 days'), from: 90 * DAY, to: 0 },
+      { title: I18n('Last 6 months'), from: 6 * MONTH, to: 0 },
+      { title: I18n('Last 1 year'), from: YEAR, to: 0 },
+      { title: I18n('Last 2 years'), from: 2 * YEAR, to: 0 },
+      { title: I18n('Last 5 years'), from: 5 * YEAR, to: 0 }
+    ],
+    [
+      { title: I18n('Last 5 minutes'), from: 5 * MINUTE, to: 0 },
+      { title: I18n('Last 15 minutes'), from: 15 * MINUTE, to: 0 },
+      { title: I18n('Last 30 minutes'), from: 30 * MINUTE, to: 0 },
+      { title: I18n('Last 1 hour'), from: HOUR, to: 0 },
+      { title: I18n('Last 3 hours'), from: 3 * HOUR, to: 0 },
+      { title: I18n('Last 6 hours'), from: 6 * HOUR, to: 0 },
+      { title: I18n('Last 12 hours'), from: 12 * HOUR, to: 0 },
+      { title: I18n('Last 24 hours'), from: DAY, to: 0 }
+    ],
+    [
+      { title: I18n('Today'), from: TODAY_START, to: 0 },
+      { title: I18n('Yesterday'), from: TODAY_START + DAY, to: TODAY_START - 1 },
+      { title: I18n('This week'), from: WEEK_START, to: 0 },
+      { title: I18n('Last week'), from: WEEK_START + WEEK, to: WEEK_START - 1 },
+      { title: I18n('This month'), from: MONTH_START, to: 0 },
+      { title: I18n('Last month'), from: MONTH_START + MONTH, to: MONTH_START - 1 },
+      { title: I18n('This year'), from: YEAR_START, to: 0 },
+      { title: I18n('Last year'), from: YEAR_START + YEAR, to: YEAR_START - 1 }
+    ]
+  ];
+
+  @Component({
+    components: { Datepicker, DropdownPanel, HueButton, HueLink }
+  })
+  export default class DateRangePicker extends Vue {
+    @Prop({ required: false, default: false })
+    inline?: boolean;
+
+    rangeSets = RANGE_SETS;
+    selectedRange: Range = RANGE_SETS[0][0];
+
+    customRange: Range = {
+      title: I18n('Custom Range'),
+      from: RANGE_NOW.toMillis() - this.selectedRange.from,
+      to: RANGE_NOW.toMillis(),
+      custom: true
+    };
+
+    get customFrom(): number | undefined {
+      if (this.selectedRange.custom) {
+        return this.selectedRange.from;
+      }
+    }
+
+    get customTo(): number | undefined {
+      if (this.selectedRange.custom) {
+        return this.selectedRange.to;
+      }
+    }
+
+    setCustomFrom(from?: Date): void {
+      if (from) {
+        Vue.set(this.customRange, 'from', from.valueOf());
+        if (this.customRange.from > this.customRange.to) {
+          Vue.set(this.customRange, 'to', this.customRange.from);
+        }
+        this.selectedRange = this.customRange;
+      }
+    }
+
+    setCustomTo(to?: Date): void {
+      if (to) {
+        Vue.set(this.customRange, 'to', to.valueOf());
+        if (this.customRange.to < this.customRange.from) {
+          Vue.set(this.customRange, 'from', this.customRange.to);
+        }
+        this.selectedRange = this.customRange;
+      }
+    }
+
+    apply(closePanel: () => void): void {
+      let range: Range;
+      if (this.selectedRange.custom) {
+        range = {
+          title: this.selectedRange.title,
+          from: DateTime.fromMillis(this.selectedRange.from).startOf('day').valueOf(),
+          to: DateTime.fromMillis(this.selectedRange.to).endOf('day').valueOf(),
+          custom: true
+        };
+      } else {
+        const now = DateTime.utc().valueOf();
+        range = {
+          title: this.selectedRange.title,
+          from: now - this.selectedRange.from,
+          to: now - this.selectedRange.to
+        };
+      }
+      this.$emit('date-range-changed', range);
+      closePanel();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import 'styles/colors';
+  @import 'styles/mixins';
+
+  .date-range-picker-panel {
+    height: 250px;
+    width: 700px;
+
+    .date-range-picker-body {
+      display: flex;
+      flex-direction: row;
+      border-bottom: 1px solid $hue-border-color;
+
+      .date-range-picker-preset,
+      .date-range-picker-custom {
+        header {
+          font-weight: 500;
+          margin-bottom: 15px;
+        }
+      }
+
+      .date-range-picker-preset {
+        flex: 0 0 340px;
+        padding: 15px 20px;
+
+        .preset-list {
+          display: flex;
+          flex-direction: row;
+
+          .preset-column {
+            flex: 0 0 33%;
+            font-size: 13px;
+            line-height: 16px;
+
+            .preset-value {
+              padding: 1px 2px;
+
+              &.selected {
+                background-color: $fluid-gray-100;
+              }
+            }
+          }
+        }
+      }
+
+      .date-range-picker-custom {
+        flex: 0 0 240px;
+        padding: 15px 20px;
+
+        .range-header {
+          color: $fluid-gray-500;
+          margin-bottom: 5px;
+          text-transform: uppercase;
+        }
+
+        /deep/ .range-input {
+          padding: 4px;
+          height: 28px;
+          line-height: 24px;
+          width: 100%;
+          font-size: 13px;
+          border: 1px solid $fluid-gray-800;
+          border-radius: 3px;
+        }
+
+        /deep/ .range-calendar {
+          width: 230px;
+
+          .cell {
+            line-height: 30px;
+            height: 30px;
+          }
+        }
+      }
+    }
+
+    .date-range-picker-footer {
+      text-align: right;
+      padding: 5px;
+    }
+  }
+</style>

+ 288 - 0
desktop/core/src/desktop/js/components/__snapshots__/DateRangePicker.test.ts.snap

@@ -0,0 +1,288 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DateRangePicker.vue should render 1`] = `
+<div
+  class="hue-dropdown-panel"
+>
+  <button
+    class="btn"
+    type="button"
+  >
+     Last 7 days 
+    <i
+      class="fa fa-caret-down"
+    />
+  </button>
+   
+  <div
+    class="hue-dropdown-container"
+  >
+    <div
+      class="hue-dropdown-inner"
+    >
+      <div
+        class="date-range-picker-panel"
+      >
+        <div
+          class="date-range-picker-body"
+        >
+          <div
+            class="date-range-picker-preset"
+          >
+            <div
+              style="border-right: 1px solid gray;"
+            >
+              <header>
+                Quick Ranges
+              </header>
+               
+              <div
+                class="preset-list"
+              >
+                <div
+                  class="preset-column"
+                >
+                  <div
+                    class="preset-value selected"
+                  >
+                    <hue-link-stub>
+                      Last 7 days
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 30 days
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 60 days
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 90 days
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 6 months
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 1 year
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 2 years
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 5 years
+                    </hue-link-stub>
+                  </div>
+                </div>
+                <div
+                  class="preset-column"
+                >
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 5 minutes
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 15 minutes
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 30 minutes
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 1 hour
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 3 hours
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 6 hours
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 12 hours
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last 24 hours
+                    </hue-link-stub>
+                  </div>
+                </div>
+                <div
+                  class="preset-column"
+                >
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Today
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Yesterday
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      This week
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last week
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      This month
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last month
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      This year
+                    </hue-link-stub>
+                  </div>
+                  <div
+                    class="preset-value"
+                  >
+                    <hue-link-stub>
+                      Last year
+                    </hue-link-stub>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+           
+          <div
+            class="date-range-picker-custom"
+          >
+            <header>
+              Time Range
+            </header>
+             
+            <div>
+              <div
+                class="range-header"
+              >
+                FROM:
+              </div>
+               
+              <datepicker-stub
+                calendarclass="range-calendar"
+                format="dd/MM/yyyy"
+                inputclass="range-input"
+                language="[object Object]"
+                maximumview="year"
+                minimumview="day"
+                placeholder="DD/MM/YYYY"
+                typeable="true"
+              />
+            </div>
+             
+            <div>
+              <div
+                class="range-header"
+              >
+                TO:
+              </div>
+               
+              <datepicker-stub
+                calendarclass="range-calendar"
+                format="dd/MM/yyyy"
+                inputclass="range-input"
+                language="[object Object]"
+                maximumview="year"
+                minimumview="day"
+                placeholder="DD/MM/YYYY"
+                typeable="true"
+              />
+            </div>
+          </div>
+        </div>
+         
+        <div
+          class="date-range-picker-footer"
+        >
+          <hue-button-stub>
+            Cancel
+          </hue-button-stub>
+           
+          <hue-button-stub>
+            Apply
+          </hue-button-stub>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;