diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index e4420d9f7bd..d318bf36f5d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -39,6 +39,7 @@ "lodash": "4.17.15", "moment": "2.24.0", "papaparse": "4.6.3", + "rc-cascader": "0.17.5", "rc-drawer": "3.0.2", "rc-time-picker": "^3.7.2", "react": "16.8.6", diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx new file mode 100644 index 00000000000..3133afce19b --- /dev/null +++ b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { text, boolean, object } from '@storybook/addon-knobs'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { Cascader } from './Cascader'; + +const getKnobs = () => { + return { + disabled: boolean('Disabled', false), + text: text('Button Text', 'Click me!'), + options: object('Options', [ + { label: 'A', value: 'A', children: [{ label: 'B', value: 'B' }, { label: 'C', value: 'C' }] }, + { label: 'D', value: 'D' }, + ]), + }; +}; + +const CascaderStories = storiesOf('UI/Cascader', module); + +CascaderStories.addDecorator(withCenteredStory); + +CascaderStories.add('default', () => { + const { disabled, text, options } = getKnobs(); + return ; +}); diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx new file mode 100644 index 00000000000..50fa30cce68 --- /dev/null +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +// @ts-ignore +import RCCascader from 'rc-cascader'; + +export interface CascaderOption { + label: string; + value: string; + + children?: CascaderOption[]; + disabled?: boolean; +} + +export interface CascaderProps { + options: CascaderOption[]; + buttonText: string; + disabled?: boolean; + expandIcon?: React.ReactNode; + value?: string[]; + + loadData?: (selectedOptions: CascaderOption[]) => void; + onChange?: (value: string[], selectedOptions: CascaderOption[]) => void; + onPopupVisibleChange?: (visible: boolean) => void; +} + +export const Cascader: React.FC = props => ( + + + +); diff --git a/packages/grafana-ui/src/components/Cascader/_Cascader.scss b/packages/grafana-ui/src/components/Cascader/_Cascader.scss new file mode 100644 index 00000000000..c3c4dfcf517 --- /dev/null +++ b/packages/grafana-ui/src/components/Cascader/_Cascader.scss @@ -0,0 +1,190 @@ +.rc-cascader { + font-size: 12px; + + &-menus { + font-size: 12px; + overflow: hidden; + background: $panel-bg; + position: absolute; + border: $panel-border; + border-radius: $border-radius; + box-shadow: $typeahead-shadow; + white-space: nowrap; + + &-hidden { + display: none; + } + + &.slide-up-enter, + &.slide-up-appear { + animation-duration: 0.3s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; + } + + &.slide-up-enter.slide-up-enter-active.rc-cascader-menus-placement, + &.slide-up-appear.slide-up-appear-active.rc-cascader-menus-placement { + &-bottomLeft { + animation-name: SlideUpIn; + animation-play-state: running; + } + + &-topLeft { + animation-name: SlideDownIn; + animation-play-state: running; + } + } + + &.slide-up-leave { + animation-duration: 0.3s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 1; + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-play-state: paused; + + &.slide-up-leave-active.rc-cascader-menus-placement { + &-bottomLeft { + animation-name: SlideUpOut; + animation-play-state: running; + } + + &-topLeft { + animation-name: SlideDownOut; + animation-play-state: running; + } + } + } + } + + &-menu { + display: inline-block; + /* width: 100px; */ + max-width: 50vw; + height: 192px; + list-style: none; + margin: 0; + padding: 0; + border-right: $panel-border; + overflow: auto; + + &:last-child { + border-right: 0; + } + + &-item { + height: 32px; + line-height: 32px; + padding: 0 2.5em 0 16px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: all 0.3s ease; + position: relative; + + &:hover { + background: $typeahead-selected-bg; + } + + &-disabled { + cursor: not-allowed; + color: $text-color-weak; + + &:hover { + background: transparent; + } + + &:after { + position: absolute; + right: 12px; + content: 'loading'; + color: $text-color-weak; + font-style: italic; + } + } + + &-active { + color: $typeahead-selected-color; + background: $typeahead-selected-bg; + + &:hover { + color: $typeahead-selected-color; + background: $typeahead-selected-bg; + } + } + + &-expand { + position: relative; + + &:after { + content: '>'; + font-size: 12px; + color: $text-color-weak; + position: absolute; + right: 16px; + line-height: 32px; + } + } + } + } +} + +@keyframes SlideUpIn { + 0% { + opacity: 0; + transform-origin: 0% 0%; + transform: scaleY(0.8); + } + + 100% { + opacity: 1; + transform-origin: 0% 0%; + transform: scaleY(1); + } +} + +@keyframes SlideUpOut { + 0% { + opacity: 1; + transform-origin: 0% 0%; + transform: scaleY(1); + } + + 100% { + opacity: 0; + transform-origin: 0% 0%; + transform: scaleY(0.8); + } +} + +@keyframes SlideDownIn { + 0% { + opacity: 0; + transform-origin: 0% 100%; + transform: scaleY(0.8); + } + + 100% { + opacity: 1; + transform-origin: 0% 100%; + transform: scaleY(1); + } +} + +@keyframes SlideDownOut { + 0% { + opacity: 1; + transform-origin: 0% 100%; + transform: scaleY(1); + } + + 100% { + opacity: 0; + transform-origin: 0% 100%; + transform: scaleY(0.8); + } +} diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index 87dbb7fc435..04d7ece8a3c 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1,18 +1,19 @@ +@import 'BarGauge/BarGauge'; +@import 'Cascader/Cascader'; +@import 'ColorPicker/ColorPicker'; @import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; -@import 'ThresholdsEditor/ThresholdsEditor'; -@import 'Table/Table'; -@import 'Table/TableInputCSV'; -@import 'Tooltip/Tooltip'; -@import 'Select/Select'; -@import 'PanelOptionsGroup/PanelOptionsGroup'; -@import 'PanelOptionsGrid/PanelOptionsGrid'; -@import 'ColorPicker/ColorPicker'; -@import 'ValueMappingsEditor/ValueMappingsEditor'; +@import 'Drawer/Drawer'; @import 'EmptySearchResult/EmptySearchResult'; @import 'FormField/FormField'; -@import 'BarGauge/BarGauge'; +@import 'PanelOptionsGrid/PanelOptionsGrid'; +@import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'RefreshPicker/RefreshPicker'; -@import 'TimePicker/TimePicker'; +@import 'Select/Select'; +@import 'Table/Table'; +@import 'Table/TableInputCSV'; +@import 'ThresholdsEditor/ThresholdsEditor'; @import 'TimePicker/TimeOfDayPicker'; -@import 'Drawer/Drawer'; +@import 'TimePicker/TimePicker'; +@import 'Tooltip/Tooltip'; +@import 'ValueMappingsEditor/ValueMappingsEditor'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 0ac55de0fc7..00dd99fbb51 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -13,6 +13,7 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer'; export { NoOptionsMessage } from './Select/NoOptionsMessage'; export { default as resetSelectStyles } from './Select/resetSelectStyles'; export { ButtonSelect } from './Select/ButtonSelect'; +export { Cascader, CascaderOption } from './Cascader/Cascader'; // Forms export { FormLabel } from './FormLabel/FormLabel'; diff --git a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx index 49052f8ce78..a7ca8831075 100644 --- a/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx +++ b/public/app/plugins/datasource/influxdb/components/InfluxLogsQueryField.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { ExploreQueryFieldProps } from '@grafana/data'; -// @ts-ignore -import Cascader from 'rc-cascader'; +import { Cascader, CascaderOption } from '@grafana/ui'; import InfluxQueryModel from '../influx_query_model'; import { AdHocFilterField, KeyValuePair } from 'app/features/explore/AdHocFilterField'; @@ -9,7 +8,6 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import InfluxDatasource from '../datasource'; import { InfluxQueryBuilder } from '../query_builder'; import { InfluxQuery, InfluxOptions } from '../types'; -import { CascaderOption } from '../../loki/components/LokiQueryFieldForm'; export interface Props extends ExploreQueryFieldProps {} @@ -139,15 +137,13 @@ export class InfluxLogsQueryField extends React.PureComponent {
- - + />
{measurement && ( diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index 040372434cc..1a0e68966e8 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -1,9 +1,16 @@ // Libraries import React from 'react'; -// @ts-ignore -import Cascader from 'rc-cascader'; -import { SlatePrism, TypeaheadOutput, SuggestionsState, QueryField, TypeaheadInput, BracesPlugin } from '@grafana/ui'; +import { + Cascader, + CascaderOption, + SlatePrism, + TypeaheadOutput, + SuggestionsState, + QueryField, + TypeaheadInput, + BracesPlugin, +} from '@grafana/ui'; // Utils & Services // dom also includes Element polyfills @@ -54,17 +61,10 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe return suggestion; } -export interface CascaderOption { - label: string; - value: string; - children?: CascaderOption[]; - disabled?: boolean; -} - export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps { history: LokiHistoryItem[]; syntax: Grammar; - logLabelOptions: any[]; + logLabelOptions: CascaderOption[]; syntaxLoaded: boolean; absoluteRange: AbsoluteTimeRange; onLoadOptions: (selectedOptions: CascaderOption[]) => void; @@ -150,19 +150,13 @@ export class LokiQueryFieldForm extends React.PureComponent { - if (isVisible && onLabelsRefresh) { - onLabelsRefresh(); - } - }} - > - - + onPopupVisibleChange={isVisible => isVisible && onLabelsRefresh && onLabelsRefresh()} + />
{ diff --git a/public/app/plugins/datasource/loki/components/useLokiSyntax.ts b/public/app/plugins/datasource/loki/components/useLokiSyntax.ts index a8e92bde3ac..2285de5cf2e 100644 --- a/public/app/plugins/datasource/loki/components/useLokiSyntax.ts +++ b/public/app/plugins/datasource/loki/components/useLokiSyntax.ts @@ -1,9 +1,9 @@ import { useState, useEffect } from 'react'; import Prism from 'prismjs'; import { AbsoluteTimeRange } from '@grafana/data'; +import { CascaderOption } from '@grafana/ui'; import LokiLanguageProvider from 'app/plugins/datasource/loki/language_provider'; import { useLokiLabels } from 'app/plugins/datasource/loki/components/useLokiLabels'; -import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; import { useRefMounted } from 'app/core/hooks/useRefMounted'; const PRISM_SYNTAX = 'promql'; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 4d47a2aa7e1..9cb118019cc 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -1,10 +1,16 @@ import _ from 'lodash'; import React from 'react'; -// @ts-ignore -import Cascader from 'rc-cascader'; import { Plugin } from 'slate'; -import { SlatePrism, TypeaheadInput, TypeaheadOutput, QueryField, BracesPlugin } from '@grafana/ui'; +import { + Cascader, + CascaderOption, + SlatePrism, + TypeaheadInput, + TypeaheadOutput, + QueryField, + BracesPlugin, +} from '@grafana/ui'; import Prism from 'prismjs'; @@ -93,13 +99,6 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type return suggestion; } -interface CascaderOption { - label: string; - value: string; - children?: CascaderOption[]; - disabled?: boolean; -} - interface PromQueryFieldProps extends ExploreQueryFieldProps { history: Array>; } @@ -284,11 +283,13 @@ class PromQueryField extends React.PureComponent
- - - +
'; - font-size: 12px; - color: $text-color-weak; - position: absolute; - right: 16px; - line-height: 32px; -} -@keyframes SlideUpIn { - 0% { - opacity: 0; - transform-origin: 0% 0%; - transform: scaleY(0.8); - } - 100% { - opacity: 1; - transform-origin: 0% 0%; - transform: scaleY(1); - } -} -@keyframes SlideUpOut { - 0% { - opacity: 1; - transform-origin: 0% 0%; - transform: scaleY(1); - } - 100% { - opacity: 0; - transform-origin: 0% 0%; - transform: scaleY(0.8); - } -} -@keyframes SlideDownIn { - 0% { - opacity: 0; - transform-origin: 0% 100%; - transform: scaleY(0.8); - } - 100% { - opacity: 1; - transform-origin: 0% 100%; - transform: scaleY(1); - } -} -@keyframes SlideDownOut { - 0% { - opacity: 1; - transform-origin: 0% 100%; - transform: scaleY(1); - } - 100% { - opacity: 0; - transform-origin: 0% 100%; - transform: scaleY(0.8); - } -}