Эх сурвалжийг харах

HUE-9485 [frontend] Add a FacetSelector Vue component

Johan Ahlen 5 жил өмнө
parent
commit
979e636402

+ 34 - 0
desktop/core/src/desktop/js/components/FacetSelector.d.ts

@@ -0,0 +1,34 @@
+// 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 FacetValue {
+  key: string;
+  value: number;
+}
+
+export interface Facet {
+  facetField: string;
+  values: FacetValue[];
+}
+
+export interface SearchFacet {
+  field: string;
+  values: string[];
+}
+
+export interface FacetValueLabels {
+  [value: string]: string;
+}

+ 212 - 0
desktop/core/src/desktop/js/components/FacetSelector.vue

@@ -0,0 +1,212 @@
+<!--
+  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="label" :disabled="disabled">
+    <template #contents="{ closePanel }">
+      <div class="facet-selector">
+        <div v-if="facet.values.length > 1" class="facet-select-all">
+          <div class="facet-list-entry">
+            <label>
+              <input v-model="allSelected" type="checkbox" />
+              <span v-if="allSelected">{{ I18n('Select None') }}</span>
+              <span v-else>{{ I18n('Select All') }}</span>
+            </label>
+          </div>
+        </div>
+        <div class="facet-selection">
+          <div v-for="facetValue in facet.values" :key="facetValue.key" class="facet-list-entry">
+            <label>
+              <input v-model="selectedValues" type="checkbox" :value="facetValue.key" />
+              {{ valueLabels[facetValue.key] || facetValue.key }}
+              <span v-if="facetValue.value">({{ facetValue.value }})</span>
+            </label>
+          </div>
+        </div>
+        <div class="facet-selector-actions">
+          <hue-link @click="cancel(closePanel)">{{ I18n('Cancel') }}</hue-link>
+          <hue-button
+            :small="true"
+            :primary="true"
+            :disabled="applyDisabled"
+            @click="apply(closePanel)"
+          >
+            {{ I18n('Apply') }}
+          </hue-button>
+        </div>
+      </div>
+    </template>
+  </dropdown-panel>
+</template>
+
+<script lang="ts">
+  import { Prop, Watch } from 'vue-property-decorator';
+  import I18n from '../utils/i18n';
+  import DropdownPanel from './dropdown/DropdownPanel.vue';
+  import HueButton from './HueButton.vue';
+  import HueLink from './HueLink.vue';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Facet, SearchFacet } from './FacetSelector';
+
+  @Component({
+    components: { DropdownPanel, HueButton, HueLink },
+    methods: { I18n }
+  })
+  export default class StatusFacet extends Vue {
+    @Prop({ required: false, default: () => ({}) })
+    valueLabels!: FacetLabels;
+    @Prop({ required: false, default: null })
+    fieldLabel?: string;
+    @Prop({ required: true })
+    facet!: Facet;
+    @Prop({ required: false, default: false })
+    disabled!: boolean;
+    @Prop({ required: false, default: false })
+    filterEnabled!: boolean;
+
+    selectedValues: string[] = [];
+    previousSelection: string[] = [];
+
+    lastKnownValues?: string[];
+
+    @Watch('facet.values')
+    initSelection(): void {
+      const newValues = this.facet.values.map(val => val.key);
+      if (!this.lastKnownValues) {
+        // Select all initially
+        this.selectedValues = newValues;
+        this.previousSelection = newValues;
+      } else {
+        // Keep previous selection on change
+        const selected = new Set(this.selectedValues);
+
+        // Select any new values that might have appeared
+        const oldValues = new Set(this.lastKnownValues);
+
+        this.selectedValues = newValues.filter(
+          newValue => selected.has(newValue) || !oldValues.has(newValue)
+        );
+      }
+      this.lastKnownValues = newValues;
+    }
+
+    clear(): void {
+      this.selectedValues = this.facet.values.map(val => val.key);
+      this.$emit('facet-removed', this.facet.facetField);
+    }
+
+    get allSelected(): boolean {
+      return this.selectedValues.length === this.facet.values.length;
+    }
+
+    set allSelected(val: boolean) {
+      if (val) {
+        this.selectedValues = this.facet.values.map(val => val.key);
+      } else {
+        this.selectedValues = [];
+      }
+    }
+
+    get applyDisabled(): boolean {
+      return !this.selectedValues.length;
+    }
+
+    get label(): string {
+      if (this.allSelected) {
+        return `${this.fieldLabel || this.facet.facetField}: ${I18n('All')}`;
+      }
+      if (this.selectedValues.length === 1) {
+        return `${this.fieldLabel || this.facet.facetField}: ${
+          this.valueLabels[this.selectedValues[0]] || this.selectedValues[0]
+        }`;
+      }
+      if (this.selectedValues.length === 0) {
+        return `${this.fieldLabel || this.facet.facetField}: ${I18n('None')}`;
+      }
+      return `${this.fieldLabel || this.facet.facetField}: ${I18n('Multiple')}`;
+    }
+
+    cancel(closePanel: () => void): void {
+      this.selectedValues = [...this.previousSelection];
+      closePanel();
+    }
+
+    apply(closePanel: () => void): void {
+      this.previousSelection = [...this.selectedValues];
+      if (this.allSelected) {
+        this.$emit('facet-removed', this.facet.facetField);
+      } else {
+        this.$emit('facet-changed', <SearchFacet>{
+          field: this.facet.facetField,
+          values: this.selectedValues
+        });
+      }
+      closePanel();
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import './styles/colors';
+  @import './styles/mixins';
+
+  .facet-selector {
+    width: 150px;
+    padding: 3px 0;
+
+    .facet-list-entry {
+      width: 100%;
+      padding: 3px 16px;
+      margin: 6px 0;
+    }
+
+    .facet-selector-actions {
+      line-height: 24px;
+      padding: 5px 10px;
+      margin-top: 3px;
+
+      a {
+        font-size: 12px;
+      }
+
+      button {
+        float: right;
+      }
+    }
+
+    /deep/ label {
+      margin: 0;
+    }
+
+    /deep/ input {
+      margin: 0 10px 0 0;
+    }
+
+    .facet-select-all {
+      border-bottom: 1px solid $hue-border-color;
+    }
+
+    .facet-selection {
+      border-bottom: 1px solid $hue-border-color;
+      overflow-x: hidden;
+      overflow-y: auto;
+      max-height: 350px;
+    }
+  }
+</style>

+ 2 - 1
desktop/core/src/desktop/js/components/__snapshots__/DateRangePicker.test.ts.snap

@@ -8,7 +8,8 @@ exports[`DateRangePicker.vue should render 1`] = `
     class="btn"
     type="button"
   >
-     Last 7 days 
+    
+    Last 7 days 
     <i
       class="fa fa-caret-down"
     />

+ 4 - 2
desktop/core/src/desktop/js/components/dropdown/DropdownPanel.vue

@@ -18,10 +18,12 @@
 
 <template>
   <div v-click-outside="clickOutside" class="hue-dropdown-panel">
-    <hue-link v-if="link" @click="togglePanel" :disabled="disabled">
+    <hue-link v-if="link" :disabled="disabled" @click="togglePanel">
       {{ text }} <i class="fa fa-caret-down" />
     </hue-link>
-    <hue-button v-else @click="togglePanel" :disabled="disabled"> {{ text }} <i class="fa fa-caret-down" /></hue-button>
+    <hue-button v-else :disabled="disabled" @click="togglePanel">
+      {{ text }} <i class="fa fa-caret-down" />
+    </hue-button>
     <div class="hue-dropdown-container" :class="{ open: panelOpen }">
       <div
         class="hue-dropdown-inner"

+ 4 - 2
desktop/core/src/desktop/js/components/dropdown/__snapshots__/DropdownMenu.test.ts.snap

@@ -8,7 +8,8 @@ exports[`DropdownMenu.vue should render dropdown with slots 1`] = `
     class="btn"
     type="button"
   >
-      
+    
+     
     <i
       class="fa fa-caret-down"
     />
@@ -42,7 +43,8 @@ exports[`DropdownMenu.vue should render empty dropdown 1`] = `
     class="btn"
     type="button"
   >
-      
+    
+     
     <i
       class="fa fa-caret-down"
     />

+ 4 - 2
desktop/core/src/desktop/js/components/dropdown/__snapshots__/DropdownPanel.test.ts.snap

@@ -5,7 +5,8 @@ exports[`DropdownPanel.vue should render dropdown panel with slots 1`] = `
   class="hue-dropdown-panel"
 >
   <hue-button-stub>
-      
+    
+     
     <i
       class="fa fa-caret-down"
     />
@@ -26,7 +27,8 @@ exports[`DropdownPanel.vue should render empty dropdown panel 1`] = `
   class="hue-dropdown-panel"
 >
   <hue-button-stub>
-      
+    
+     
     <i
       class="fa fa-caret-down"
     />