From b6c6854afe40b76bbe2dcad58ee2b9471f1e65a1 Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Sat, 25 Nov 2023 00:09:55 -0600 Subject: [PATCH 1/7] Render multiple drilldown forms Signed-off-by: Willie Hung --- .../vis_type_drilldown/public/card_form.tsx | 94 +++++++++++++++ .../vis_type_drilldown/public/drilldown_fn.ts | 35 +++--- .../public/drilldown_options.tsx | 110 ++++++++---------- .../public/drilldown_options_list.tsx | 26 ----- .../public/drilldown_renderer.tsx | 2 +- .../public/drilldown_vis.ts | 31 ++--- .../public/drilldown_vis_controller.tsx | 24 ++-- .../vis_type_drilldown/public/index.ts | 5 + .../vis_type_drilldown/public/to_ast.ts | 4 +- .../vis_type_drilldown/public/types.ts | 24 ++-- 10 files changed, 200 insertions(+), 155 deletions(-) create mode 100644 src/plugins/vis_type_drilldown/public/card_form.tsx delete mode 100644 src/plugins/vis_type_drilldown/public/drilldown_options_list.tsx diff --git a/src/plugins/vis_type_drilldown/public/card_form.tsx b/src/plugins/vis_type_drilldown/public/card_form.tsx new file mode 100644 index 000000000000..ba0d053cc5ca --- /dev/null +++ b/src/plugins/vis_type_drilldown/public/card_form.tsx @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiPanel, + EuiTitle, + EuiTextArea, + EuiFlexItem, + EuiFieldText, + EuiAccordion, + EuiFlexGroup, +} from '@elastic/eui'; + +interface CardFormProps { + index: number; + cards: any[]; + card: any; + onUpdateCard: (index: number, card: any) => void; +} + +const CardForm = ({ index, cards, card, onUpdateCard }: CardFormProps) => ( + + + + + +

+ +

+
+
+ + + + onUpdateCard(index, { ...cards[index], cardName: value }) + } + fullWidth={true} + /> + + + + +

+ +

+
+
+ + + + onUpdateCard(index, { ...cards[index], cardDescription: value }) + } + fullWidth={true} + data-test-subj="markdownTextarea" + /> + + + + +

+ +

+
+
+ + + + onUpdateCard(index, { ...cards[index], url: value }) + } + fullWidth={true} + /> + +
+
+
+); + +export { CardForm }; diff --git a/src/plugins/vis_type_drilldown/public/drilldown_fn.ts b/src/plugins/vis_type_drilldown/public/drilldown_fn.ts index 6579e7721239..e12f49a7ce81 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_fn.ts +++ b/src/plugins/vis_type_drilldown/public/drilldown_fn.ts @@ -5,8 +5,7 @@ import { i18n } from '@osd/i18n'; import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; -import { DrilldownVisParams } from './types'; -import { DrilldownArguments } from './types'; +import { DrilldownArguments, DrilldownVisParams } from './types'; export interface DrilldownVisRenderValue { visType: 'drilldown'; @@ -28,20 +27,6 @@ export const createDrilldownVisFn = (): DrilldownVisExpressionFunctionDefinition defaultMessage: 'Drilldown visualization', }), args: { - // font: { - // types: ['style'], - // help: i18n.translate('visTypeMarkdown.function.font.help', { - // defaultMessage: 'Font settings.', - // }), - // default: `{font size=12}`, - // }, - // openLinksInNewTab: { - // types: ['boolean'], - // default: false, - // help: i18n.translate('visTypeMarkdown.function.openLinksInNewTab.help', { - // defaultMessage: 'Opens links in new tab', - // }), - // }, cardName: { types: ['string'], aliases: ['_'], @@ -58,6 +43,20 @@ export const createDrilldownVisFn = (): DrilldownVisExpressionFunctionDefinition defaultMessage: 'Card description', }), }, + url: { + types: ['string'], + aliases: ['_'], + required: true, + help: i18n.translate('visTypeDrilldown.function.url.help', { + defaultMessage: 'URL', + }), + }, + cards: { + types: [], + help: i18n.translate('visTypeDrilldown.function.cards.help', { + defaultMessage: 'Cards', + }), + }, }, fn(input, args) { return { @@ -68,8 +67,8 @@ export const createDrilldownVisFn = (): DrilldownVisExpressionFunctionDefinition visParams: { cardName: args.cardName, cardDescription: args.cardDescription, - // openLinksInNewTab: args.openLinksInNewTab, - // fontSize: parseInt(args.font.spec.fontSize || '12', 10), + url: args.url, + cards: args.cards, }, }, }; diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx index 6060414ba4d0..49a6ef06239b 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx @@ -3,76 +3,60 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState } from 'react'; -import { - EuiPanel, - EuiTitle, - EuiTextArea, - EuiFlexGroup, - EuiFlexItem, - EuiFieldText, - EuiAccordion, -} from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { DrilldownVisParams } from './types'; +import { Card, DrilldownVisParams } from './types'; +import { CardForm } from './card_form'; function DrilldownOptions({ stateParams, setValue }: VisOptionsProps) { - const onMarkdownUpdate = useCallback( - (value: DrilldownVisParams['cardName']) => setValue('cardName', value), - [setValue] - ); - - const onDescriptionUpdate = useCallback( - (value: DrilldownVisParams['cardDescription']) => setValue('cardDescription', value), - [setValue] - ); + const [formCount, setFormCount] = useState(stateParams.cards.length ?? 1); + + const addCardForm = () => { + setFormCount(formCount + 1); + addCard(); // Also add a new card to the array + }; + + const addCard = () => { + const newCard: Card = { + cardName: '', + cardDescription: '', + url: '', + }; + setValue('cards', [...stateParams.cards, newCard]); + }; + + const updateCard = (index: number, card: any) => { + const updatedCards = [...stateParams.cards]; + updatedCards[index] = card; + setValue('cards', updatedCards); + }; return ( - - - - - -

- -

-
-
- - - onMarkdownUpdate(value)} - fullWidth={true} - /> - - - - -

- -

-
-
- - - onDescriptionUpdate(value)} - fullWidth={true} - data-test-subj="markdownTextarea" - /> - -
-
-
+ <> + + {Array.from({ length: formCount }).map((_, index) => ( + + ))} + + + + + ); } diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options_list.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options_list.tsx deleted file mode 100644 index 357a983d87bf..000000000000 --- a/src/plugins/vis_type_drilldown/public/drilldown_options_list.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import React, { useState } from 'react'; -import { EuiButton } from '@elastic/eui'; -import { DrilldownOptions } from './drilldown_options'; // Adjust the import path as needed - -function DrilldownManager() { - const [formsCount, setFormsCount] = useState(1); - - const addNewForm = () => { - setFormsCount(formsCount + 1); - }; - - return ( -
- {[...Array(formsCount)].map((_, index) => ( - - ))} - Add New Form -
- ); -} - -export { DrilldownManager }; diff --git a/src/plugins/vis_type_drilldown/public/drilldown_renderer.tsx b/src/plugins/vis_type_drilldown/public/drilldown_renderer.tsx index 1513684f3bc7..a29b8d5e6f67 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_renderer.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_renderer.tsx @@ -14,7 +14,7 @@ const DrilldownVisComponent = lazy(() => import('./drilldown_vis_controller')); export const drilldownVisRenderer: ExpressionRenderDefinition = { name: 'drilldown_vis', - displayName: 'markdown visualization', + displayName: 'drilldown visualization', reuseDomNode: true, render: async (domNode, { visParams }, handlers) => { handlers.onDestroy(() => { diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts index d6f2ee14915b..3389a417eee8 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts @@ -8,9 +8,6 @@ import { DrilldownOptions } from './drilldown_options'; import { SettingsOptions } from './settings_options_lazy'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { toExpressionAst } from './to_ast'; -import { AggGroupNames } from '../../data/public'; -import { Schemas } from '../../vis_default_editor/public'; -import { DrilldownList } from './drilldown_options_list'; export const drillDownVisDefinition = { name: 'drilldown', @@ -25,6 +22,14 @@ export const drillDownVisDefinition = { defaults: { cardName: '', cardDescription: '', + url: '', + cards: [ + { + cardName: '', + cardDescription: '', + url: '', + }, + ], }, }, editorConfig: { @@ -46,26 +51,6 @@ export const drillDownVisDefinition = { ], enableAutoApply: true, defaultSize: DefaultEditorSize.MEDIUM, - // schemas: new Schemas([ - // { - // group: AggGroupNames.Metrics, - // name: 'metric', - // title: i18n.translate('visTypeMetric.schemas.metricTitle', { defaultMessage: 'Metric' }), - // min: 1, - // aggFilter: [], - // aggSettings: { - // top_hits: { - // allowStrings: true, - // }, - // }, - // defaults: [ - // { - // type: 'count', - // schema: 'metric', - // }, - // ], - // }, - // ]), }, options: { showTimePicker: false, diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx index 0decacc58777..033621e97e9f 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx @@ -4,7 +4,6 @@ */ import React, { useEffect } from 'react'; -import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { DrilldownVisParams } from './types'; interface DrilldownVisComponentProps extends DrilldownVisParams { @@ -14,20 +13,25 @@ interface DrilldownVisComponentProps extends DrilldownVisParams { const DrilldownVisComponent = ({ cardName, cardDescription, + url, + cards, renderComplete, }: DrilldownVisComponentProps) => { useEffect(renderComplete); // renderComplete will be called after each render to signal, that we are done with rendering. return ( - - } - title={cardName} - description={cardDescription} - onClick={() => {}} - /> - + <> +

COOL

+ {/* + } + title={cardName} + description={cardDescription} + onClick={() => window.open(url, '_blank')} + /> + */} + {/*

{cardName}

*/} + ); }; diff --git a/src/plugins/vis_type_drilldown/public/index.ts b/src/plugins/vis_type_drilldown/public/index.ts index efac18d2da92..e1a0a03c0d61 100644 --- a/src/plugins/vis_type_drilldown/public/index.ts +++ b/src/plugins/vis_type_drilldown/public/index.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import './index.scss'; import { VisDrilldownPlugin } from './plugin'; diff --git a/src/plugins/vis_type_drilldown/public/to_ast.ts b/src/plugins/vis_type_drilldown/public/to_ast.ts index a53b7757a1e3..8dd2d2ee0b89 100644 --- a/src/plugins/vis_type_drilldown/public/to_ast.ts +++ b/src/plugins/vis_type_drilldown/public/to_ast.ts @@ -8,13 +8,15 @@ import { buildExpression, buildExpressionFunction } from '../../expressions/publ import { DrilldownVisExpressionFunctionDefinition } from './drilldown_fn'; export const toExpressionAst = (vis: Vis) => { - const { cardName, cardDescription } = vis.params; + const { cardName, cardDescription, url, cards } = vis.params; const drilldownVis = buildExpressionFunction( 'drilldownVis', { cardName, cardDescription, + url, + cards, } ); diff --git a/src/plugins/vis_type_drilldown/public/types.ts b/src/plugins/vis_type_drilldown/public/types.ts index b147c2745027..ce724bb5478f 100644 --- a/src/plugins/vis_type_drilldown/public/types.ts +++ b/src/plugins/vis_type_drilldown/public/types.ts @@ -18,24 +18,22 @@ export interface AppPluginStartDependencies { visualizations: VisualizationsSetup; } -// export interface Arguments { -// markdown: string; -// font: Style; -// openLinksInNewTab: boolean; -// } - -// export interface DrilldownVisParams { -// markdown: Arguments['markdown']; -// openLinksInNewTab: Arguments['openLinksInNewTab']; -// fontSize: number; -// } +export interface Card { + cardName: string; + cardDescription: string; + url: string; +} export interface DrilldownArguments { cardName: string; cardDescription: string; + url: string; + cards: Card[]; } export interface DrilldownVisParams { - cardName: DrilldownArguments['cardName']; - cardDescription: DrilldownArguments['cardDescription']; + cardName: string; + cardDescription: string; + url: string; + cards: Card[]; } From 0452554c2dd62f502a2bce4484c2a16678a92f68 Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Wed, 29 Nov 2023 15:29:51 -0600 Subject: [PATCH 2/7] Remove unuse import/reference and implement multiple forms Signed-off-by: Willie Hung --- src/plugins/vis_type_drilldown/.eslintrc.js | 7 - src/plugins/vis_type_drilldown/.i18nrc.json | 7 - src/plugins/vis_type_drilldown/README.md | 9 +- .../opensearch_dashboards.json | 10 +- .../vis_type_drilldown/public/application.tsx | 28 ---- .../vis_type_drilldown/public/card_form.tsx | 143 +++++++++--------- .../public/components/app.tsx | 124 --------------- .../vis_type_drilldown/public/drilldown_fn.ts | 29 +--- .../public/drilldown_options.tsx | 34 ++--- .../public/drilldown_vis.ts | 7 +- .../public/drilldown_vis_controller.tsx | 32 ++-- .../vis_type_drilldown/public/plugin.ts | 36 +---- .../vis_type_drilldown/public/to_ast.ts | 5 +- .../vis_type_drilldown/public/types.ts | 15 +- 14 files changed, 115 insertions(+), 371 deletions(-) delete mode 100644 src/plugins/vis_type_drilldown/.eslintrc.js delete mode 100644 src/plugins/vis_type_drilldown/.i18nrc.json delete mode 100644 src/plugins/vis_type_drilldown/public/application.tsx delete mode 100644 src/plugins/vis_type_drilldown/public/components/app.tsx diff --git a/src/plugins/vis_type_drilldown/.eslintrc.js b/src/plugins/vis_type_drilldown/.eslintrc.js deleted file mode 100644 index b16a8b23a08e..000000000000 --- a/src/plugins/vis_type_drilldown/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - rules: { - '@osd/eslint/require-license-header': 'off', - }, -}; diff --git a/src/plugins/vis_type_drilldown/.i18nrc.json b/src/plugins/vis_type_drilldown/.i18nrc.json deleted file mode 100644 index 138c287b908a..000000000000 --- a/src/plugins/vis_type_drilldown/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "visTypeDrilldown", - "paths": { - "visTypeDrilldown": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/src/plugins/vis_type_drilldown/README.md b/src/plugins/vis_type_drilldown/README.md index a35b1f5ece8d..062c2781fb0f 100755 --- a/src/plugins/vis_type_drilldown/README.md +++ b/src/plugins/vis_type_drilldown/README.md @@ -1,11 +1,6 @@ # vis_type_drilldown -A OpenSearch Dashboards plugin +This drilldown plugin allows users to design their own navigation controls, customized to fit their specific preferences and workflow requirements. +Please refer to [Drilldown functionality for dashboards](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1308) --- - -## Development - -See the [OpenSearch Dashboards contributing -guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions -setting up your development environment. diff --git a/src/plugins/vis_type_drilldown/opensearch_dashboards.json b/src/plugins/vis_type_drilldown/opensearch_dashboards.json index c7cb6af3f0fa..250d7660d44b 100644 --- a/src/plugins/vis_type_drilldown/opensearch_dashboards.json +++ b/src/plugins/vis_type_drilldown/opensearch_dashboards.json @@ -4,14 +4,6 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": [ - "opensearchDashboardsReact", - "charts", - "expressions", - "visualizations", - "navigation", - "data", - "visDefaultEditor" - ], + "requiredPlugins": ["charts", "expressions", "visualizations", "visDefaultEditor"], "optionalPlugins": [] } diff --git a/src/plugins/vis_type_drilldown/public/application.tsx b/src/plugins/vis_type_drilldown/public/application.tsx deleted file mode 100644 index 7184c1ab807c..000000000000 --- a/src/plugins/vis_type_drilldown/public/application.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { AppMountParameters, CoreStart } from '../../../core/public'; -import { AppPluginStartDependencies } from './types'; -import { VisDrilldownApp } from './components/app'; - -export const renderApp = ( - { notifications, http }: CoreStart, - { navigation }: AppPluginStartDependencies, - { appBasePath, element }: AppMountParameters -) => { - ReactDOM.render( - , - element - ); - - return () => ReactDOM.unmountComponentAtNode(element); -}; diff --git a/src/plugins/vis_type_drilldown/public/card_form.tsx b/src/plugins/vis_type_drilldown/public/card_form.tsx index ba0d053cc5ca..27bf46b8033c 100644 --- a/src/plugins/vis_type_drilldown/public/card_form.tsx +++ b/src/plugins/vis_type_drilldown/public/card_form.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiPanel, EuiTitle, @@ -13,82 +13,89 @@ import { EuiAccordion, EuiFlexGroup, } from '@elastic/eui'; +import { Card } from './types'; interface CardFormProps { index: number; - cards: any[]; - card: any; - onUpdateCard: (index: number, card: any) => void; + card: Card; + updateCard: (index: number, card: Card) => void; } -const CardForm = ({ index, cards, card, onUpdateCard }: CardFormProps) => ( - - - - - -

- -

-
-
+const CardForm = ({ index, card, updateCard }: CardFormProps) => { + return ( + + + + + +

+ +

+
+
- - - onUpdateCard(index, { ...cards[index], cardName: value }) - } - fullWidth={true} - /> - + + { + updateCard(index, { ...card, cardName: value }); + }} + fullWidth={true} + /> + - - -

- -

-
-
+ + +

+ +

+
+
- - - onUpdateCard(index, { ...cards[index], cardDescription: value }) - } - fullWidth={true} - data-test-subj="markdownTextarea" - /> - + + { + updateCard(index, { ...card, cardDescription: value }); + }} + fullWidth={true} + data-test-subj="markdownTextarea" + /> + - - -

- -

-
-
+ + +

+ +

+
+
- - - onUpdateCard(index, { ...cards[index], url: value }) - } - fullWidth={true} - /> - -
-
-
-); + + { + updateCard(index, { ...card, cardUrl: value }); + }} + fullWidth={true} + /> + +
+
+
+ ); +}; export { CardForm }; diff --git a/src/plugins/vis_type_drilldown/public/components/app.tsx b/src/plugins/vis_type_drilldown/public/components/app.tsx deleted file mode 100644 index c059e51219a1..000000000000 --- a/src/plugins/vis_type_drilldown/public/components/app.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; -import { BrowserRouter as Router } from 'react-router-dom'; - -import { - EuiButton, - EuiHorizontalRule, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiPageHeader, - EuiTitle, - EuiText, -} from '@elastic/eui'; - -import { CoreStart } from '../../../../core/public'; -import { NavigationPublicPluginStart } from '../../../navigation/public'; - -import { PLUGIN_ID, PLUGIN_NAME } from '../../common'; - -interface VisDrilldownAppDeps { - basename: string; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - navigation: NavigationPublicPluginStart; -} - -export const VisDrilldownApp = ({ - basename, - notifications, - http, - navigation, -}: VisDrilldownAppDeps) => { - // Use React hooks to manage state. - const [timestamp, setTimestamp] = useState(); - - const onClickHandler = () => { - // Use the core http service to make a response to the server API. - http.get('/api/vis_type_drilldown/example').then((res) => { - setTimestamp(res.time); - // Use the core notifications service to display a success message. - notifications.toasts.addSuccess( - i18n.translate('visTypeDrilldown.dataUpdated', { - defaultMessage: 'Data updated', - }) - ); - }); - }; - - // Render the application DOM. - // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. - return ( - - - <> - - - - - -

- -

-
-
- - - -

- -

-
-
- - -

- -

- -

- -

- - - -
-
-
-
-
- -
-
- ); -}; diff --git a/src/plugins/vis_type_drilldown/public/drilldown_fn.ts b/src/plugins/vis_type_drilldown/public/drilldown_fn.ts index e12f49a7ce81..80d08c8bf22e 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_fn.ts +++ b/src/plugins/vis_type_drilldown/public/drilldown_fn.ts @@ -23,34 +23,10 @@ export const createDrilldownVisFn = (): DrilldownVisExpressionFunctionDefinition name: 'drilldownVis', type: 'render', inputTypes: [], - help: i18n.translate('visDrilldown.function.help', { + help: i18n.translate('visTypeDrilldown.function.help', { defaultMessage: 'Drilldown visualization', }), args: { - cardName: { - types: ['string'], - aliases: ['_'], - required: true, - help: i18n.translate('visTypeDrilldown.function.cardName.help', { - defaultMessage: 'Card name', - }), - }, - cardDescription: { - types: ['string'], - aliases: ['_'], - required: true, - help: i18n.translate('visTypeDrilldown.function.cardDescription.help', { - defaultMessage: 'Card description', - }), - }, - url: { - types: ['string'], - aliases: ['_'], - required: true, - help: i18n.translate('visTypeDrilldown.function.url.help', { - defaultMessage: 'URL', - }), - }, cards: { types: [], help: i18n.translate('visTypeDrilldown.function.cards.help', { @@ -65,9 +41,6 @@ export const createDrilldownVisFn = (): DrilldownVisExpressionFunctionDefinition value: { visType: 'drilldown', visParams: { - cardName: args.cardName, - cardDescription: args.cardDescription, - url: args.url, cards: args.cards, }, }, diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx index 49a6ef06239b..18121470d496 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; -import { EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { EuiFlexGroup, EuiButtonEmpty, EuiFieldText } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; @@ -12,23 +12,16 @@ import { Card, DrilldownVisParams } from './types'; import { CardForm } from './card_form'; function DrilldownOptions({ stateParams, setValue }: VisOptionsProps) { - const [formCount, setFormCount] = useState(stateParams.cards.length ?? 1); - const addCardForm = () => { - setFormCount(formCount + 1); - addCard(); // Also add a new card to the array - }; - - const addCard = () => { const newCard: Card = { - cardName: '', - cardDescription: '', - url: '', + cardName: 'newDrilldownCard', + cardDescription: 'newDrilldownCard', + cardUrl: 'newDrilldownCard', }; setValue('cards', [...stateParams.cards, newCard]); }; - const updateCard = (index: number, card: any) => { + const updateCard = (index: number, card: Card) => { const updatedCards = [...stateParams.cards]; updatedCards[index] = card; setValue('cards', updatedCards); @@ -43,15 +36,12 @@ function DrilldownOptions({ stateParams, setValue }: VisOptionsProps - {Array.from({ length: formCount }).map((_, index) => ( - - ))} + {stateParams.cards && + stateParams.cards.map((card, index) => ( + <> + + + ))} diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts index 3389a417eee8..df685e296173 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts @@ -13,21 +13,18 @@ export const drillDownVisDefinition = { name: 'drilldown', title: 'Drilldown', isAccessible: true, - icon: 'dashboardApp', + icon: 'logstashFilter', description: i18n.translate('visTypeMarkdown.markdownDescription', { defaultMessage: 'I LOVE drilldown!', }), toExpressionAst, visConfig: { defaults: { - cardName: '', - cardDescription: '', - url: '', cards: [ { cardName: '', cardDescription: '', - url: '', + cardUrl: '', }, ], }, diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx index 033621e97e9f..9067ce077818 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx @@ -4,33 +4,31 @@ */ import React, { useEffect } from 'react'; +import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { DrilldownVisParams } from './types'; interface DrilldownVisComponentProps extends DrilldownVisParams { renderComplete: () => void; } -const DrilldownVisComponent = ({ - cardName, - cardDescription, - url, - cards, - renderComplete, -}: DrilldownVisComponentProps) => { +const DrilldownVisComponent = ({ cards, renderComplete }: DrilldownVisComponentProps) => { useEffect(renderComplete); // renderComplete will be called after each render to signal, that we are done with rendering. return ( <> -

COOL

- {/* - } - title={cardName} - description={cardDescription} - onClick={() => window.open(url, '_blank')} - /> - */} - {/*

{cardName}

*/} + {cards && + cards.map((card, index) => ( + + } + title={card.cardName} + layout="horizontal" + description={card.cardDescription} + onClick={() => window.open(card.cardUrl, '_blank')} + paddingSize="m" + /> + + ))} ); }; diff --git a/src/plugins/vis_type_drilldown/public/plugin.ts b/src/plugins/vis_type_drilldown/public/plugin.ts index 70efda116ff7..d2297f883614 100644 --- a/src/plugins/vis_type_drilldown/public/plugin.ts +++ b/src/plugins/vis_type_drilldown/public/plugin.ts @@ -3,14 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { i18n } from '@osd/i18n'; -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../core/public'; -import { - VisDrilldownPluginSetup, - VisDrilldownPluginStart, - AppPluginStartDependencies, -} from './types'; -import { PLUGIN_NAME } from '../common'; +import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { VisDrilldownPluginSetup, VisDrilldownPluginStart } from './types'; import { drillDownVisDefinition } from './drilldown_vis'; import { createDrilldownVisFn } from './drilldown_fn'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -31,32 +25,6 @@ export class VisDrilldownPlugin visualizations.createBaseVisualization(drillDownVisDefinition); expressions.registerRenderer(drilldownVisRenderer); expressions.registerFunction(createDrilldownVisFn); - - // Register an application into the side navigation menu - core.application.register({ - id: 'visTypeDrilldown', - title: PLUGIN_NAME, - async mount(params: AppMountParameters) { - // Load application bundle - const { renderApp } = await import('./application'); - // Get start services as specified in opensearch_dashboards.json - const [coreStart, depsStart] = await core.getStartServices(); - // Render the application - return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); - }, - }); - - // Return methods that should be available to other plugins - return { - getGreeting() { - return i18n.translate('visTypeDrilldown.greetingText', { - defaultMessage: 'Hello from {name}!', - values: { - name: PLUGIN_NAME, - }, - }); - }, - }; } public start(core: CoreStart): VisDrilldownPluginStart { diff --git a/src/plugins/vis_type_drilldown/public/to_ast.ts b/src/plugins/vis_type_drilldown/public/to_ast.ts index 8dd2d2ee0b89..c7230b1a68cd 100644 --- a/src/plugins/vis_type_drilldown/public/to_ast.ts +++ b/src/plugins/vis_type_drilldown/public/to_ast.ts @@ -8,14 +8,11 @@ import { buildExpression, buildExpressionFunction } from '../../expressions/publ import { DrilldownVisExpressionFunctionDefinition } from './drilldown_fn'; export const toExpressionAst = (vis: Vis) => { - const { cardName, cardDescription, url, cards } = vis.params; + const { cards } = vis.params; const drilldownVis = buildExpressionFunction( 'drilldownVis', { - cardName, - cardDescription, - url, cards, } ); diff --git a/src/plugins/vis_type_drilldown/public/types.ts b/src/plugins/vis_type_drilldown/public/types.ts index ce724bb5478f..20ee1542b1e5 100644 --- a/src/plugins/vis_type_drilldown/public/types.ts +++ b/src/plugins/vis_type_drilldown/public/types.ts @@ -5,11 +5,10 @@ import { NavigationPublicPluginStart } from '../../navigation/public'; import { VisualizationsSetup } from '../../visualizations/public'; -import { Arguments } from '../../vis_type_markdown/public/types'; -export interface VisDrilldownPluginSetup { - getGreeting: () => string; -} +// export interface VisDrilldownPluginSetup { +// getGreeting: () => string; +// } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisDrilldownPluginStart {} @@ -21,19 +20,13 @@ export interface AppPluginStartDependencies { export interface Card { cardName: string; cardDescription: string; - url: string; + cardUrl: string; } export interface DrilldownArguments { - cardName: string; - cardDescription: string; - url: string; cards: Card[]; } export interface DrilldownVisParams { - cardName: string; - cardDescription: string; - url: string; cards: Card[]; } From 93f08deb021b8dd3f59fc820971ffe89d1388ed1 Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Wed, 29 Nov 2023 18:04:27 -0600 Subject: [PATCH 3/7] Implement multiple drilldown forms & remove unused import Signed-off-by: Willie Hung --- .../public/components/card_form.tsx | 101 ++++++++++++++++++ .../public/drilldown_options.tsx | 25 +++-- .../public/drilldown_vis.ts | 6 +- .../public/drilldown_vis_controller.tsx | 3 +- 4 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 src/plugins/vis_type_drilldown/public/components/card_form.tsx diff --git a/src/plugins/vis_type_drilldown/public/components/card_form.tsx b/src/plugins/vis_type_drilldown/public/components/card_form.tsx new file mode 100644 index 000000000000..90ce371d196d --- /dev/null +++ b/src/plugins/vis_type_drilldown/public/components/card_form.tsx @@ -0,0 +1,101 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiPanel, + EuiTitle, + EuiTextArea, + EuiFlexItem, + EuiFieldText, + EuiAccordion, + EuiFlexGroup, +} from '@elastic/eui'; +import { Card } from '../types'; + +interface CardFormProps { + index: number; + card: Card; + updateCard: (index: number, card: Card) => void; +} + +const CardForm = ({ index, card, updateCard }: CardFormProps) => { + return ( + + + + + +

+ +

+
+
+ + + { + updateCard(index, { ...card, cardName: value }); + }} + fullWidth={true} + /> + + + + +

+ +

+
+
+ + + { + updateCard(index, { ...card, cardDescription: value }); + }} + fullWidth={true} + data-test-subj="markdownTextarea" + /> + + + + +

+ +

+
+
+ + + { + updateCard(index, { ...card, cardUrl: value }); + }} + fullWidth={true} + /> + +
+
+
+ ); +}; + +export { CardForm }; diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx index 18121470d496..dc6e4ebd4c02 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx @@ -3,29 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiButtonEmpty, EuiFieldText } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Card, DrilldownVisParams } from './types'; -import { CardForm } from './card_form'; +import { CardForm } from './components/card_form'; function DrilldownOptions({ stateParams, setValue }: VisOptionsProps) { - const addCardForm = () => { + const updateCard = useCallback( + (index: number, card: Card) => { + const updatedCards = [...stateParams.cards]; + updatedCards[index] = card; + setValue('cards', updatedCards); + }, + [stateParams.cards, setValue] + ); + + const addCardForm = useCallback(() => { const newCard: Card = { cardName: 'newDrilldownCard', cardDescription: 'newDrilldownCard', cardUrl: 'newDrilldownCard', }; setValue('cards', [...stateParams.cards, newCard]); - }; - - const updateCard = (index: number, card: Card) => { - const updatedCards = [...stateParams.cards]; - updatedCards[index] = card; - setValue('cards', updatedCards); - }; + }, [stateParams.cards, setValue]); return ( <> diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts index df685e296173..0c4489de1203 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis.ts +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis.ts @@ -14,7 +14,7 @@ export const drillDownVisDefinition = { title: 'Drilldown', isAccessible: true, icon: 'logstashFilter', - description: i18n.translate('visTypeMarkdown.markdownDescription', { + description: i18n.translate('visTypeDrilldown.drilldownDescription', { defaultMessage: 'I LOVE drilldown!', }), toExpressionAst, @@ -33,14 +33,14 @@ export const drillDownVisDefinition = { optionTabs: [ { name: 'advanced', - title: i18n.translate('visTypeMarkdown.tabs.dataText', { + title: i18n.translate('visTypeDrilldown.tabs.dataText', { defaultMessage: 'Data', }), editor: DrilldownOptions, }, { name: 'options', - title: i18n.translate('visTypeMarkdown.tabs.optionsText', { + title: i18n.translate('visTypeDrilldown.tabs.optionsText', { defaultMessage: 'Options', }), editor: SettingsOptions, diff --git a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx index 9067ce077818..17fbd61616eb 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_vis_controller.tsx @@ -5,9 +5,10 @@ import React, { useEffect } from 'react'; import { EuiCard, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import { DrilldownVisParams } from './types'; +import { DrilldownVisParams, Card } from './types'; interface DrilldownVisComponentProps extends DrilldownVisParams { + cards: Card[]; renderComplete: () => void; } From 1fa7e7be2da859b3e30cb9d93b0642c1087236de Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Wed, 29 Nov 2023 18:25:36 -0600 Subject: [PATCH 4/7] Remove duplicate files Signed-off-by: Willie Hung --- .../vis_type_drilldown/public/card_form.tsx | 101 ------------------ 1 file changed, 101 deletions(-) delete mode 100644 src/plugins/vis_type_drilldown/public/card_form.tsx diff --git a/src/plugins/vis_type_drilldown/public/card_form.tsx b/src/plugins/vis_type_drilldown/public/card_form.tsx deleted file mode 100644 index 27bf46b8033c..000000000000 --- a/src/plugins/vis_type_drilldown/public/card_form.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { - EuiPanel, - EuiTitle, - EuiTextArea, - EuiFlexItem, - EuiFieldText, - EuiAccordion, - EuiFlexGroup, -} from '@elastic/eui'; -import { Card } from './types'; - -interface CardFormProps { - index: number; - card: Card; - updateCard: (index: number, card: Card) => void; -} - -const CardForm = ({ index, card, updateCard }: CardFormProps) => { - return ( - - - - - -

- -

-
-
- - - { - updateCard(index, { ...card, cardName: value }); - }} - fullWidth={true} - /> - - - - -

- -

-
-
- - - { - updateCard(index, { ...card, cardDescription: value }); - }} - fullWidth={true} - data-test-subj="markdownTextarea" - /> - - - - -

- -

-
-
- - - { - updateCard(index, { ...card, cardUrl: value }); - }} - fullWidth={true} - /> - -
-
-
- ); -}; - -export { CardForm }; From 4c9005c5dc7c38f3dab21128ff90deccb2039634 Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Wed, 29 Nov 2023 19:11:31 -0600 Subject: [PATCH 5/7] Resolve conflicts & remove unused imports Signed-off-by: Willie Hung --- .../opensearch_dashboards.json | 8 +- .../public/drilldown_options.tsx | 121 +++++++----------- 2 files changed, 55 insertions(+), 74 deletions(-) diff --git a/src/plugins/vis_type_drilldown/opensearch_dashboards.json b/src/plugins/vis_type_drilldown/opensearch_dashboards.json index 250d7660d44b..0c07d5b60c16 100644 --- a/src/plugins/vis_type_drilldown/opensearch_dashboards.json +++ b/src/plugins/vis_type_drilldown/opensearch_dashboards.json @@ -4,6 +4,12 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["charts", "expressions", "visualizations", "visDefaultEditor"], + "requiredPlugins": [ + "charts", + "expressions", + "visualizations", + "visDefaultEditor", + "opensearchDashboardsReact" + ], "optionalPlugins": [] } diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx index 31dd89c42489..0205ea8c9e4a 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx @@ -3,22 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ - -import React, { useCallback, Fragment, useState, useEffect, useRef } from 'react'; +import React, { useCallback, Fragment, useEffect, useRef } from 'react'; import { - EuiPanel, EuiTitle, - EuiTextArea, EuiFlexGroup, EuiFlexItem, - EuiFieldText, - EuiAccordion, EuiSuperSelect, EuiText, + EuiButtonEmpty, } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { useOpenSearchDashboards } from '../../opensearch_dashboards_react/public'; -import { DrilldownServices, DrilldownVisParams } from './types'; +import { Card, DrilldownServices, DrilldownVisParams } from './types'; +import { CardForm } from './components/card_form'; function DrilldownOptions({ stateParams, setValue }: VisOptionsProps) { const updateCard = useCallback( @@ -30,6 +28,15 @@ function DrilldownOptions({ stateParams, setValue }: VisOptionsProps { + const newCard: Card = { + cardName: 'newDrilldownCard', + cardDescription: 'newDrilldownCard', + cardUrl: 'newDrilldownCard', + }; + setValue('cards', [...stateParams.cards, newCard]); + }, [stateParams.cards, setValue]); + const { services: { http, savedObjects }, } = useOpenSearchDashboards(); @@ -60,14 +67,14 @@ function DrilldownOptions({ stateParams, setValue }: VisOptionsProps(); - const index = useRef(); + // const index = useRef(); useEffect(() => { const fetchData = async () => { saved.current = savedObjects?.client.find({ type: 'dashboard', }); - const path = (await saved.current).savedObjects[0]['client'] + const path = (await saved.current).savedObjects[0].client .getPath(['dashboard', (await saved.current).savedObjects[0].id]) .substring(28); const savedObjectURL = http.basePath.prepend('/app/dashboards#/view/' + path); @@ -91,76 +98,44 @@ function DrilldownOptions({ stateParams, setValue }: VisOptionsProps setValue('cardDescription', value), - [setValue] - ); + }); const activeVisName = ''; const handleVisTypeChange = () => {}; return ( - - - - - -

- -

-
-
- - - onMarkdownUpdate(value)} - fullWidth={true} + + {stateParams.cards && + stateParams.cards.map((card, index) => ( + <> + + + +

+ +

+
+
+ + -
- - - -

- -

-
-
- - - onDescriptionUpdate(value)} - fullWidth={true} - data-test-subj="markdownTextarea" - /> - - - - -

- -

-
-
- - -
-
-
+ + ))} + + + + ); } From 41dc3aa0f86853b27cef42fb03f1f4b5b0a25d11 Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Wed, 29 Nov 2023 23:28:31 -0600 Subject: [PATCH 6/7] Integrated the destination select into the drilldown form Signed-off-by: Willie Hung --- .../public/components/card_form.tsx | 29 ++++++++++++++++++- .../public/drilldown_options.tsx | 16 +++------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/plugins/vis_type_drilldown/public/components/card_form.tsx b/src/plugins/vis_type_drilldown/public/components/card_form.tsx index 90ce371d196d..329f06325efe 100644 --- a/src/plugins/vis_type_drilldown/public/components/card_form.tsx +++ b/src/plugins/vis_type_drilldown/public/components/card_form.tsx @@ -12,6 +12,7 @@ import { EuiFieldText, EuiAccordion, EuiFlexGroup, + EuiSuperSelect, } from '@elastic/eui'; import { Card } from '../types'; @@ -19,9 +20,19 @@ interface CardFormProps { index: number; card: Card; updateCard: (index: number, card: Card) => void; + options: any; + activeVisName: string; + handleVisTypeChange: () => void; } -const CardForm = ({ index, card, updateCard }: CardFormProps) => { +const CardForm = ({ + index, + card, + updateCard, + options, + valueOfSelected, + onChange, +}: CardFormProps) => { return ( { fullWidth={true} /> + + + +

+ +

+
+
+ +
diff --git a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx index 0205ea8c9e4a..49afe579658a 100644 --- a/src/plugins/vis_type_drilldown/public/drilldown_options.tsx +++ b/src/plugins/vis_type_drilldown/public/drilldown_options.tsx @@ -114,21 +114,13 @@ function DrilldownOptions({ stateParams, setValue }: VisOptionsProps ( <> - - - -

- -

-
-
- - ))} From 05855cb01a0f5e81a69066dc380de17924f28acc Mon Sep 17 00:00:00 2001 From: Willie Hung Date: Thu, 30 Nov 2023 15:27:20 -0600 Subject: [PATCH 7/7] Fix canvas rendering bug Signed-off-by: Willie Hung --- .../vis_type_drilldown/public/components/card_form.tsx | 6 +++--- src/plugins/vis_type_drilldown/public/drilldown_fn.ts | 2 +- .../vis_type_drilldown/public/drilldown_vis_controller.tsx | 7 ++++--- src/plugins/vis_type_drilldown/public/to_ast.ts | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_type_drilldown/public/components/card_form.tsx b/src/plugins/vis_type_drilldown/public/components/card_form.tsx index 329f06325efe..8df23869db5a 100644 --- a/src/plugins/vis_type_drilldown/public/components/card_form.tsx +++ b/src/plugins/vis_type_drilldown/public/components/card_form.tsx @@ -21,8 +21,8 @@ interface CardFormProps { card: Card; updateCard: (index: number, card: Card) => void; options: any; - activeVisName: string; - handleVisTypeChange: () => void; + valueOfSelected: string; + onChange: () => void; } const CardForm = ({ @@ -35,7 +35,7 @@ const CardForm = ({ }: CardFormProps) => { return ( void; } const DrilldownVisComponent = ({ cards, renderComplete }: DrilldownVisComponentProps) => { useEffect(renderComplete); // renderComplete will be called after each render to signal, that we are done with rendering. + const parsedCardData = JSON.parse(cards); + return ( <> - {cards && - cards.map((card, index) => ( + {parsedCardData && + parsedCardData.map((card: Card, index: number) => ( } diff --git a/src/plugins/vis_type_drilldown/public/to_ast.ts b/src/plugins/vis_type_drilldown/public/to_ast.ts index c7230b1a68cd..b830336b56b0 100644 --- a/src/plugins/vis_type_drilldown/public/to_ast.ts +++ b/src/plugins/vis_type_drilldown/public/to_ast.ts @@ -13,7 +13,7 @@ export const toExpressionAst = (vis: Vis) => { const drilldownVis = buildExpressionFunction( 'drilldownVis', { - cards, + cards: JSON.stringify(cards), } );