Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add layer and panel part of cluster layer #408

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export const MAP_SAVED_OBJECT_TYPE = 'map';
// TODO: Replace with actual app icon
export const MAPS_APP_ICON = 'gisApp';
export const MAPS_VISUALIZATION_DESCRIPTION = 'Create map visualization with multiple layers';
export const CLUSTER_DEFAULT_FILL_TYPE = 'gradient';
export const CLUSTER_DEFAULT_PALETTE = 'pallette_1';
export const CLUSTER_MIN_DEFAULT_RADIUS_SIZE = 50;
export const CLUSTER_MAX_DEFAULT_RADIUS_SIZE = 200;
export const CLUSTER_MIN_BORDER_THICKNESS = 0;
export const CLUSTER_MAX_BORDER_THICKNESS = 100;
export const CLUSTER_DEFAULT_MARKER_BORDER_THICKNESS = 1;

// Starting position [lng, lat] and zoom
export const MAP_INITIAL_STATE = {
Expand All @@ -91,12 +98,14 @@ export enum DASHBOARDS_MAPS_LAYER_NAME {
OPENSEARCH_MAP = 'OpenSearch map',
DOCUMENTS = 'Documents',
CUSTOM_MAP = 'Custom map',
CLUSTER = 'Cluster',
}

export enum DASHBOARDS_MAPS_LAYER_TYPE {
OPENSEARCH_MAP = 'opensearch_vector_tile_map',
DOCUMENTS = 'documents',
CUSTOM_MAP = 'custom_map',
CLUSTER = 'cluster',
}

export enum DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE {
Expand All @@ -108,12 +117,15 @@ export enum DASHBOARDS_MAPS_LAYER_ICON {
OPENSEARCH_MAP = 'globe',
DOCUMENTS = 'document',
CUSTOM_MAP = 'globe',
CLUSTER = 'heatmap',
}

export enum DASHBOARDS_MAPS_LAYER_DESCRIPTION {
OPENSEARCH_MAP = 'Use default OpenSearch basemaps.',
DOCUMENTS = 'View points, lines, and polygons on the map.',
CUSTOM_MAP = 'Configure maps to use a custom map source.',
//TODO: wait ux and writer for content
CLUSTER = 'cluster layer',
}

export const DOCUMENTS = {
Expand All @@ -137,6 +149,13 @@ export const CUSTOM_MAP = {
description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.CUSTOM_MAP,
};

export const CLUSTER = {
name: DASHBOARDS_MAPS_LAYER_NAME.CLUSTER,
type: DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER,
icon: DASHBOARDS_MAPS_LAYER_ICON.CLUSTER,
description: DASHBOARDS_MAPS_LAYER_DESCRIPTION.CLUSTER,
};

export interface Layer {
name: DASHBOARDS_MAPS_LAYER_NAME;
type: DASHBOARDS_MAPS_LAYER_TYPE;
Expand All @@ -153,6 +172,7 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = {
[DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP]: 'globe',
[DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS]: 'document',
[DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe',
[DASHBOARDS_MAPS_LAYER_TYPE.CLUSTER]: 'heatmap',
};

// refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages
Expand Down
2 changes: 1 addition & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"opensearchDashboardsVersion": "3.0.0",
"server": true,
"ui": true,
"requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations"],
"requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations","visDefaultEditor"],
"optionalPlugins": ["home"]
}
3 changes: 2 additions & 1 deletion public/components/add_layer_panel/add_layer_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
Layer,
NEW_MAP_LAYER_DEFAULT_PREFIX,
MAX_LAYER_LIMIT,
CLUSTER,
} from '../../../common';
import { getLayerConfigMap } from '../../utils/getIntialConfig';
import { ConfigSchema } from '../../../common/config';
Expand Down Expand Up @@ -67,7 +68,7 @@ export const AddLayerPanel = ({
addLayer(initLayerConfig);
}

const dataLayers = [DOCUMENTS];
const dataLayers = [CLUSTER, DOCUMENTS];
const dataLayerItems = Object.values(dataLayers).map((layerItem, index) => {
return (
<EuiKeyPadMenuItem
Expand Down
87 changes: 87 additions & 0 deletions public/components/layer_config/cluster_config/agg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback, useEffect } from 'react';
import { DefaultEditorAggParams } from '../../../../../../src/plugins/vis_default_editor/public';
import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state';
import { IAggConfig } from '../../../../../../src/plugins/data/public';
import { IndexPattern } from '../../../../../../src/plugins/data/common';
import { AggCommonProps } from './agg_common_props';

interface Props extends AggCommonProps {
agg: IAggConfig;
indexPattern: IndexPattern | null | undefined;
aggIndex: number;
setAggsState: React.Dispatch<AggsAction>;
}

export const Agg = ({
agg,
aggIndex,
indexPattern,
schemas,
groupName,
metricAggs,
setAggParamValue,
onAggTypeChange,
state,
setAggsState,
formIsTouched,
timeRange,
}: Props) => {
const aggName = agg?.type?.name;
const setValidity = useCallback(
(isValid: boolean) => {
setAggsState({
type: AGGS_ACTION_KEYS.VALID,
payload: isValid,
aggId: agg.id,
});
},
[agg.id, setAggsState]
);
const setTouched = useCallback(
(touched: boolean) => {
setAggsState({
type: AGGS_ACTION_KEYS.TOUCHED,
payload: touched,
aggId: agg.id,
});
},
[agg.id, setAggsState]
);

// This useEffect is required to update the timeRange value and initiate rerender to keep labels up to date (Issue #57822).
useEffect(() => {
if (timeRange && aggName === 'date_histogram') {
agg?.aggConfigs?.setTimeRange(timeRange);
}
}, [agg, aggName, timeRange]);

//DefaultEditorAggParams needs indexPattern to render,but it can display a fallback state when we pass a fake indexPattern.
const fallbackIndexPattern = {
getAggregationRestrictions: () => {
return undefined;
},
} as unknown as IndexPattern;

return (
<DefaultEditorAggParams
className="vbConfig__aggEditor"
agg={agg}
aggIndex={aggIndex}
indexPattern={indexPattern ?? fallbackIndexPattern}
setValidity={setValidity}
setTouched={setTouched}
schemas={schemas}
formIsTouched={formIsTouched}
groupName={groupName}
metricAggs={metricAggs}
state={state}
setAggParamValue={setAggParamValue}
onAggTypeChange={onAggTypeChange}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { IndexPattern, AggGroupName, TimeRange } from '../../../../../../src/plugins/data/common';
import { Schema } from '../../../../../../src/plugins/vis_default_editor/public';
import { IAggConfig, IAggType } from '../../../../../../src/plugins/data/public';
import { type DefaultEmptyState } from './cluster_layer_source';

export interface AggCommonProps {
indexPattern: IndexPattern | null | undefined;
schemas: Schema[];
groupName: AggGroupName;
metricAggs: IAggConfig[];
setAggParamValue: <T extends keyof IAggConfig['params']>(
aggId: IAggConfig['id'],
paramName: T,
value: IAggConfig['params'][T]
) => void;
onAggTypeChange: (aggId: IAggConfig['id'], aggType: IAggType) => void;
state: DefaultEmptyState;
timeRange?: TimeRange;
formIsTouched: boolean;
}
105 changes: 105 additions & 0 deletions public/components/layer_config/cluster_config/agg_group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useMemo, useEffect, useReducer } from 'react';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { IAggConfig } from '../../../../../../src/plugins/data/public';
import { Agg } from './agg';
import {
aggGroupReducer,
initAggsState,
AGGS_ACTION_KEYS,
isInvalidAggsTouched,
} from './agg_group_state';
import { AggCommonProps } from './agg_common_props';

interface Props extends AggCommonProps {
aggs: IAggConfig[];
setValidity(modelName: string, value: boolean): void;
setTouched(isTouched: boolean): void;
}

const GROUP_NAME_LABELS = {
metrics: 'Metrics',
buckets: 'Cluster',
none: '',
};

export const AggGroup = ({
aggs,
indexPattern,
schemas,
groupName,
setValidity,
setAggParamValue,
formIsTouched,
onAggTypeChange,
state,
metricAggs,
setTouched,
timeRange,
}: Props) => {
const schemaNames = schemas.map((s) => s.name);
const group: IAggConfig[] = useMemo(
() => aggs.filter((agg: IAggConfig) => agg.schema && schemaNames.includes(agg.schema)) || [],
[aggs, schemaNames]
);
const [aggsState, setAggsState] = useReducer(aggGroupReducer, group, initAggsState);
const isGroupValid = Object.values(aggsState).every((item) => item.valid);
const isAllAggsTouched = isInvalidAggsTouched(aggsState);

useEffect(() => {
// when isAllAggsTouched is true, it means that all invalid aggs are touched and we will set ngModel's touched to true
// which indicates that Apply button can be changed to Error button (when all invalid ngModels are touched)
setTouched(isAllAggsTouched);
}, [isAllAggsTouched, setTouched]);

useEffect(() => {
// when not all invalid aggs are touched and formIsTouched becomes true, it means that Apply button was clicked.
// and in such case we set touched state to true for all aggs
if (formIsTouched && !isAllAggsTouched) {
Object.keys(aggsState).map(([aggId]) => {
setAggsState({
type: AGGS_ACTION_KEYS.TOUCHED,
payload: true,
aggId,
});
});
}
}, [formIsTouched]);

useEffect(() => {
setValidity(`aggGroup__${groupName}`, isGroupValid);
}, [groupName, isGroupValid, setValidity]);

return (
<>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h3>{GROUP_NAME_LABELS[groupName]}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<>
{group.map((agg: IAggConfig, index: number) => (
<Agg
agg={agg}
aggIndex={index}
indexPattern={indexPattern}
schemas={schemas}
groupName={groupName}
metricAggs={metricAggs}
setAggParamValue={setAggParamValue}
onAggTypeChange={onAggTypeChange}
state={state}
setAggsState={setAggsState}
formIsTouched={aggsState[agg.id] ? aggsState[agg.id].touched : false}
timeRange={timeRange}
/>
))}
</>
</EuiPanel>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { IAggConfig } from '../../../../../../src/plugins/data/public';
import { isEmpty } from 'lodash';

export enum AGGS_ACTION_KEYS {
TOUCHED = 'aggsTouched',
VALID = 'aggsValid',
}

interface AggsItem {
touched: boolean;
valid: boolean;
}

export interface AggsState {
[aggId: string]: AggsItem;
}

export interface AggsAction {
type: AGGS_ACTION_KEYS;
payload: boolean;
aggId: string;
newState?: AggsState;
}

function aggGroupReducer(state: AggsState, action: AggsAction): AggsState {
const aggState = state[action.aggId] || { touched: false, valid: true };
switch (action.type) {
case AGGS_ACTION_KEYS.TOUCHED:
return { ...state, [action.aggId]: { ...aggState, touched: action.payload } };
case AGGS_ACTION_KEYS.VALID:
return { ...state, [action.aggId]: { ...aggState, valid: action.payload } };
default:
throw new Error();
}
}

function initAggsState(group: IAggConfig[]): AggsState {
return group.reduce((state, agg) => {
state[agg.id] = { touched: false, valid: true };
return state;
}, {} as AggsState);
}

function isInvalidAggsTouched(aggsState: AggsState) {
const invalidAggs = Object.values(aggsState).filter((agg) => !agg.valid);

if (isEmpty(invalidAggs)) {
return false;
}

return invalidAggs.every((agg) => agg.touched);
}

export { aggGroupReducer, initAggsState, isInvalidAggsTouched };
Loading
Loading