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

[data view mgmt] add saved object relationships to data view management #132385

Merged
merged 14 commits into from
May 24, 2022
2 changes: 1 addition & 1 deletion src/plugins/data_view_management/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["management", "data", "urlForwarding", "dataViewFieldEditor", "dataViewEditor", "dataViews", "fieldFormats", "unifiedSearch"],
"requiredPlugins": ["management", "data", "urlForwarding", "dataViewFieldEditor", "dataViewEditor", "dataViews", "fieldFormats", "unifiedSearch", "savedObjectsManagement"],
"requiredBundles": ["kibanaReact", "kibanaUtils"],
"optionalPlugins": ["spaces"],
"owner": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export const TAB_INDEXED_FIELDS = 'indexedFields';
export const TAB_SCRIPTED_FIELDS = 'scriptedFields';
export const TAB_SOURCE_FILTERS = 'sourceFilters';
export const TAB_RELATIONSHIPS = 'relationships';
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
SavedObjectRelation,
SavedObjectManagementTypeInfo,
} from '@kbn/saved-objects-management-plugin/public';
import { IndexPatternManagmentContext } from '../../types';
import { Tabs } from './tabs';
import { IndexHeader } from './index_header';
Expand All @@ -32,6 +37,10 @@ export interface EditIndexPatternProps extends RouteComponentProps {
indexPattern: DataView;
}

export interface SavedObjectRelationWithTitle extends SavedObjectRelation {
title: string;
}

const mappingAPILink = i18n.translate(
'indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink',
{
Expand All @@ -57,14 +66,34 @@ const securitySolution = 'security-solution';

export const EditIndexPattern = withRouter(
({ indexPattern, history, location }: EditIndexPatternProps) => {
const { uiSettings, overlays, chrome, dataViews } =
const { uiSettings, overlays, chrome, dataViews, savedObjectsManagement } =
useKibana<IndexPatternManagmentContext>().services;
const [fields, setFields] = useState<DataViewField[]>(indexPattern.getNonScriptedFields());
const [conflictedFields, setConflictedFields] = useState<DataViewField[]>(
indexPattern.fields.getAll().filter((field) => field.type === 'conflict')
);
const [defaultIndex, setDefaultIndex] = useState<string>(uiSettings.get('defaultIndex'));
const [tags, setTags] = useState<any[]>([]);
const [relationships, setRelationships] = useState<SavedObjectRelationWithTitle[]>([]);
const [allowedTypes, setAllowedTypes] = useState<SavedObjectManagementTypeInfo[]>([]);

useEffect(() => {
savedObjectsManagement.getAllowedTypes().then((resp) => {
setAllowedTypes(resp);
});
}, [savedObjectsManagement]);

useEffect(() => {
if (allowedTypes.length === 0) {
return;
}
const allowedAsString = allowedTypes.map((item) => item.name);
savedObjectsManagement
.getRelationships(DATA_VIEW_SAVED_OBJECT_TYPE, indexPattern.id!, allowedAsString)
.then((resp) => {
setRelationships(resp.relations.map((r) => ({ ...r, title: r.meta.title! })));
});
}, [savedObjectsManagement, indexPattern, allowedTypes]);

useEffect(() => {
setFields(indexPattern.getNonScriptedFields());
Expand Down Expand Up @@ -200,6 +229,8 @@ export const EditIndexPattern = withRouter(
indexPattern={indexPattern}
saveIndexPattern={dataViews.updateSavedObject.bind(dataViews)}
fields={fields}
relationships={relationships}
allowedTypes={allowedTypes}
history={history}
location={location}
refreshFields={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { i18n } from '@kbn/i18n';

export const typeFieldName = i18n.translate(
'indexPatternManagement.objectsTable.relationships.columnTypeName',
{
defaultMessage: 'Type',
}
);

export const typeFieldDescription = i18n.translate(
'indexPatternManagement.objectsTable.relationships.columnTypeDescription',
{ defaultMessage: 'Type of the saved object' }
);

export const titleFieldName = i18n.translate(
'indexPatternManagement.objectsTable.relationships.columnTitleName',
{
defaultMessage: 'Title',
}
);

export const titleFieldDescription = i18n.translate(
'indexPatternManagement.objectsTable.relationships.columnTitleDescription',
{ defaultMessage: 'Title of the saved object' }
);

export const filterTitle = i18n.translate(
'indexPatternManagement.objectsTable.relationships.search.filters.type.name',
{ defaultMessage: 'Type' }
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export { RelationshipsTable } from './relationships_table';
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import {
EuiInMemoryTable,
HorizontalAlignment,
EuiText,
EuiLink,
EuiTableDataType,
} from '@elastic/eui';
import { CoreStart } from '@kbn/core/public';
import { get } from 'lodash';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';

import {
SavedObjectRelation,
SavedObjectManagementTypeInfo,
SavedObjectsManagementPluginStart,
} from '@kbn/saved-objects-management-plugin/public';

import { EuiToolTip, EuiIcon, SearchFilterConfig } from '@elastic/eui';
import { IPM_APP_ID } from '../../../plugin';
import {
typeFieldName,
typeFieldDescription,
titleFieldName,
titleFieldDescription,
filterTitle,
} from './i18n';

const canGoInApp = (
savedObject: SavedObjectRelation,
capabilities: CoreStart['application']['capabilities']
) => {
const { inAppUrl } = savedObject.meta;
if (!inAppUrl) return false;
if (!inAppUrl.uiCapabilitiesPath) return true;
return Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath));
};

export const RelationshipsTable = ({
basePath,
capabilities,
id,
navigateToUrl,
getDefaultTitle,
getSavedObjectLabel,
relationships,
allowedTypes,
}: {
basePath: CoreStart['http']['basePath'];
capabilities: CoreStart['application']['capabilities'];
navigateToUrl: CoreStart['application']['navigateToUrl'];
id: string;
getDefaultTitle: SavedObjectsManagementPluginStart['getDefaultTitle'];
getSavedObjectLabel: SavedObjectsManagementPluginStart['getSavedObjectLabel'];
relationships: SavedObjectRelation[];
allowedTypes: SavedObjectManagementTypeInfo[];
}) => {
const columns = [
{
field: 'type',
name: typeFieldName,
width: '50px',
align: 'center' as HorizontalAlignment,
description: typeFieldDescription,
sortable: false,
render: (type: string, object: SavedObjectRelation) => {
const typeLabel = getSavedObjectLabel(type, allowedTypes);
return (
<EuiToolTip position="top" content={typeLabel}>
<EuiIcon
aria-label={typeLabel}
type={object.meta.icon || 'apps'}
size="s"
data-test-subj="relationshipsObjectType"
/>
</EuiToolTip>
);
},
},
{
field: 'title',
name: titleFieldName,
description: titleFieldDescription,
dataType: 'string' as EuiTableDataType,
sortable: false,
render: (title: string, object: SavedObjectRelation) => {
const path = object.meta.inAppUrl?.path || '';
const showUrl = canGoInApp(object, capabilities);
const titleDisplayed = title || getDefaultTitle(object);

return showUrl ? (
<EuiLink href={basePath.prepend(path)} data-test-subj="relationshipsTitle">
{titleDisplayed}
</EuiLink>
) : (
<EuiText size="s" data-test-subj="relationshipsTitle">
{titleDisplayed}
</EuiText>
);
},
},
];

const filterTypesMap = new Map(
relationships.map((relationship) => [
relationship.type,
{
value: relationship.type,
name: relationship.type,
view: relationship.type,
},
])
);

const search = {
// query,
// onChange: handleOnChange,
box: {
incremental: true,
schema: true,
},
filters: [
{
type: 'field_value_selection',
field: 'type',
name: filterTitle,
multiSelect: 'or',
options: [...filterTypesMap.values()],
},
] as SearchFilterConfig[],
};

return (
<RedirectAppLinks currentAppId={IPM_APP_ID} navigateToUrl={navigateToUrl}>
<EuiInMemoryTable<SavedObjectRelation>
items={relationships}
columns={columns}
pagination={true}
search={search}
rowProps={() => ({
'data-test-subj': `relationshipsTableRow`,
})}
/>
</RedirectAppLinks>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@ import {
DataViewsPublicPluginStart,
META_FIELDS,
} from '@kbn/data-views-plugin/public';
import {
SavedObjectRelation,
SavedObjectManagementTypeInfo,
} from '@kbn/saved-objects-management-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { IndexPatternManagmentContext } from '../../../types';
import { createEditIndexPatternPageStateContainer } from '../edit_index_pattern_state_container';
import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants';
import {
TAB_INDEXED_FIELDS,
TAB_SCRIPTED_FIELDS,
TAB_SOURCE_FILTERS,
TAB_RELATIONSHIPS,
} from '../constants';
import { SourceFiltersTable } from '../source_filters_table';
import { IndexedFieldsTable } from '../indexed_fields_table';
import { ScriptedFieldsTable } from '../scripted_fields_table';
import { RelationshipsTable } from '../relationships_table';
import { getTabs, getPath, convertToEuiFilterOptions } from './utils';
import { getFieldInfo } from '../../utils';

Expand All @@ -45,6 +55,8 @@ interface TabsProps extends Pick<RouteComponentProps, 'history' | 'location'> {
fields: DataViewField[];
saveIndexPattern: DataViewsPublicPluginStart['updateSavedObject'];
refreshFields: () => void;
relationships: SavedObjectRelation[];
allowedTypes: SavedObjectManagementTypeInfo[];
}

interface FilterItems {
Expand Down Expand Up @@ -131,9 +143,20 @@ export function Tabs({
history,
location,
refreshFields,
relationships,
allowedTypes,
}: TabsProps) {
const { uiSettings, docLinks, dataViewFieldEditor, overlays, theme, dataViews } =
useKibana<IndexPatternManagmentContext>().services;
const {
uiSettings,
docLinks,
dataViewFieldEditor,
overlays,
theme,
dataViews,
http,
application,
savedObjectsManagement,
} = useKibana<IndexPatternManagmentContext>().services;
const [fieldFilter, setFieldFilter] = useState<string>('');
const [syncingStateFunc, setSyncingStateFunc] = useState<any>({
getCurrentTab: () => TAB_INDEXED_FIELDS,
Expand Down Expand Up @@ -492,6 +515,22 @@ export function Tabs({
/>
</Fragment>
);
case TAB_RELATIONSHIPS:
return (
<Fragment>
<EuiSpacer size="m" />
<RelationshipsTable
basePath={http.basePath}
id={indexPattern.id!}
capabilities={application.capabilities}
relationships={relationships}
allowedTypes={allowedTypes}
navigateToUrl={application.navigateToUrl}
getDefaultTitle={savedObjectsManagement.getDefaultTitle}
getSavedObjectLabel={savedObjectsManagement.getSavedObjectLabel}
/>
</Fragment>
);
}
},
[
Expand All @@ -513,18 +552,25 @@ export function Tabs({
overlays,
theme,
dataViews,
http,
application,
savedObjectsManagement,
allowedTypes,
relationships,
]
);

const euiTabs: EuiTabbedContentTab[] = useMemo(
() =>
getTabs(indexPattern, fieldFilter).map((tab: Pick<EuiTabbedContentTab, 'name' | 'id'>) => {
return {
...tab,
content: getContent(tab.id),
};
}),
[fieldFilter, getContent, indexPattern]
getTabs(indexPattern, fieldFilter, relationships.length).map(
(tab: Pick<EuiTabbedContentTab, 'name' | 'id'>) => {
return {
...tab,
content: getContent(tab.id),
};
}
),
[fieldFilter, getContent, indexPattern, relationships]
);

const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id);
Expand Down
Loading