[NP] Graph: get rid of saved objects class wrapper (#59917) (#61057)

* Implement find saved workspace

* Implement get saved workspace

* Create helper function as applyESResp

* Fix eslint

* Implement savedWorkspaceLoader.get()

* Implement deleteWS

* Implement saveWS

* Remove applyESRespUtil

* Refactoring

* Refactoring

* Remove savedWorkspaceLoader

* Update unit test

* Fix merge conflicts

* Add unit tests for saveWithConfirmation

* Fix TS

* Move saveWithConfirmation to a separate file

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Maryia Lapata 2020-03-24 17:52:16 +03:00 committed by GitHub
parent 7c4c3b165c
commit 8edfd14da5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 478 additions and 180 deletions

View file

@ -21,7 +21,13 @@ import { SavedObjectsPublicPlugin } from './plugin';
export { OnSaveProps, SavedObjectSaveModal, SaveResult, showSaveModal } from './save_modal';
export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder';
export { SavedObjectLoader, createSavedObjectClass } from './saved_object';
export {
SavedObjectLoader,
createSavedObjectClass,
checkForDuplicateTitle,
saveWithConfirmation,
isErrorNonFatal,
} from './saved_object';
export { SavedObjectSaveOpts, SavedObjectKibanaServices, SavedObject } from './types';
export const plugin = () => new SavedObjectsPublicPlugin();

View file

@ -30,7 +30,7 @@ import { checkForDuplicateTitle } from './check_for_duplicate_title';
* @param error {Error} the error
* @return {boolean}
*/
function isErrorNonFatal(error: { message: string }) {
export function isErrorNonFatal(error: { message: string }) {
if (!error) return false;
return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED;
}

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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.
*/
import { SavedObjectAttributes, SavedObjectsCreateOptions, OverlayStart } from 'kibana/public';
import { SavedObjectsClientContract } from '../../../../../core/public';
import { saveWithConfirmation } from './save_with_confirmation';
import * as deps from './confirm_modal_promise';
import { OVERWRITE_REJECTED } from '../../constants';
describe('saveWithConfirmation', () => {
const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract;
const overlays: OverlayStart = {} as OverlayStart;
const source: SavedObjectAttributes = {} as SavedObjectAttributes;
const options: SavedObjectsCreateOptions = {} as SavedObjectsCreateOptions;
const savedObject = {
getEsType: () => 'test type',
title: 'test title',
displayName: 'test display name',
};
beforeEach(() => {
savedObjectsClient.create = jest.fn();
jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.resolve({} as any));
});
test('should call create of savedObjectsClient', async () => {
await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays });
expect(savedObjectsClient.create).toHaveBeenCalledWith(
savedObject.getEsType(),
source,
options
);
});
test('should call confirmModalPromise when such record exists', async () => {
savedObjectsClient.create = jest
.fn()
.mockImplementation((type, src, opt) =>
opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } })
);
await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays });
expect(deps.confirmModalPromise).toHaveBeenCalledWith(
expect.any(String),
expect.any(String),
expect.any(String),
overlays
);
});
test('should call create of savedObjectsClient when overwriting confirmed', async () => {
savedObjectsClient.create = jest
.fn()
.mockImplementation((type, src, opt) =>
opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } })
);
await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays });
expect(savedObjectsClient.create).toHaveBeenLastCalledWith(savedObject.getEsType(), source, {
overwrite: true,
...options,
});
});
test('should reject when overwriting denied', async () => {
savedObjectsClient.create = jest.fn().mockReturnValue(Promise.reject({ res: { status: 409 } }));
jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.reject());
expect.assertions(1);
await expect(
saveWithConfirmation(source, savedObject, options, {
savedObjectsClient,
overlays,
})
).rejects.toThrow(OVERWRITE_REJECTED);
});
});

View file

@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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.
*/
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import {
SavedObjectAttributes,
SavedObjectsCreateOptions,
OverlayStart,
SavedObjectsClientContract,
} from 'kibana/public';
import { OVERWRITE_REJECTED } from '../../constants';
import { confirmModalPromise } from './confirm_modal_promise';
/**
* Attempts to create the current object using the serialized source. If an object already
* exists, a warning message requests an overwrite confirmation.
* @param source - serialized version of this object what will be indexed into elasticsearch.
* @param savedObject - a simple object that contains properties title and displayName, and getEsType method
* @param options - options to pass to the saved object create method
* @param services - provides Kibana services savedObjectsClient and overlays
* @returns {Promise} - A promise that is resolved with the objects id if the object is
* successfully indexed. If the overwrite confirmation was rejected, an error is thrown with
* a confirmRejected = true parameter so that case can be handled differently than
* a create or index error.
* @resolved {SavedObject}
*/
export async function saveWithConfirmation(
source: SavedObjectAttributes,
savedObject: {
getEsType(): string;
title: string;
displayName: string;
},
options: SavedObjectsCreateOptions,
services: { savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart }
) {
const { savedObjectsClient, overlays } = services;
try {
return await savedObjectsClient.create(savedObject.getEsType(), source, options);
} catch (err) {
// record exists, confirm overwriting
if (get(err, 'res.status') === 409) {
const confirmMessage = i18n.translate(
'savedObjects.confirmModal.overwriteConfirmationMessage',
{
defaultMessage: 'Are you sure you want to overwrite {title}?',
values: { title: savedObject.title },
}
);
const title = i18n.translate('savedObjects.confirmModal.overwriteTitle', {
defaultMessage: 'Overwrite {name}?',
values: { name: savedObject.displayName },
});
const confirmButtonText = i18n.translate('savedObjects.confirmModal.overwriteButtonLabel', {
defaultMessage: 'Overwrite',
});
return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays)
.then(() =>
savedObjectsClient.create(savedObject.getEsType(), source, {
overwrite: true,
...options,
})
)
.catch(() => Promise.reject(new Error(OVERWRITE_REJECTED)));
}
return await Promise.reject(err);
}
}

View file

@ -19,3 +19,6 @@
export { createSavedObjectClass } from './saved_object';
export { SavedObjectLoader } from './saved_object_loader';
export { checkForDuplicateTitle } from './helpers/check_for_duplicate_title';
export { saveWithConfirmation } from './helpers/save_with_confirmation';
export { isErrorNonFatal } from './helpers/save_saved_object';

View file

@ -31,6 +31,11 @@ import { asAngularSyncedObservable } from './helpers/as_observable';
import { colorChoices } from './helpers/style_choices';
import { createGraphStore, datasourceSelector, hasFieldsSelector } from './state_management';
import { formatHttpError } from './helpers/format_http_error';
import {
findSavedWorkspace,
getSavedWorkspace,
deleteSavedWorkspace,
} from './helpers/saved_workspace_utils';
export function initGraphApp(angularModule, deps) {
const {
@ -42,7 +47,6 @@ export function initGraphApp(angularModule, deps) {
getBasePath,
data,
config,
savedWorkspaceLoader,
capabilities,
coreStart,
storage,
@ -112,15 +116,21 @@ export function initGraphApp(angularModule, deps) {
$location.url(getNewPath());
};
$scope.find = search => {
return savedWorkspaceLoader.find(search, $scope.listingLimit);
return findSavedWorkspace(
{ savedObjectsClient, basePath: coreStart.http.basePath },
search,
$scope.listingLimit
);
};
$scope.editItem = workspace => {
$location.url(getEditPath(workspace));
};
$scope.getViewUrl = workspace => getEditUrl(addBasePath, workspace);
$scope.delete = workspaces => {
return savedWorkspaceLoader.delete(workspaces.map(({ id }) => id));
};
$scope.delete = workspaces =>
deleteSavedWorkspace(
savedObjectsClient,
workspaces.map(({ id }) => id)
);
$scope.capabilities = capabilities;
$scope.initialFilter = $location.search().filter || '';
$scope.coreStart = coreStart;
@ -133,7 +143,7 @@ export function initGraphApp(angularModule, deps) {
resolve: {
savedWorkspace: function($rootScope, $route, $location) {
return $route.current.params.id
? savedWorkspaceLoader.get($route.current.params.id).catch(function(e) {
? getSavedWorkspace(savedObjectsClient, $route.current.params.id).catch(function(e) {
toastNotifications.addError(e, {
title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', {
defaultMessage: "Couldn't load graph with ID",
@ -146,7 +156,7 @@ export function initGraphApp(angularModule, deps) {
// return promise that never returns to prevent the controller from loading
return new Promise();
})
: savedWorkspaceLoader.get();
: getSavedWorkspace(savedObjectsClient);
},
indexPatterns: function() {
return savedObjectsClient
@ -283,6 +293,8 @@ export function initGraphApp(angularModule, deps) {
},
notifications: coreStart.notifications,
http: coreStart.http,
overlays: coreStart.overlays,
savedObjectsClient,
showSaveModal,
setWorkspaceInitialized: () => {
$scope.workspaceInitialized = true;

View file

@ -29,7 +29,6 @@ import { Plugin as DataPlugin, IndexPatternsContract } from '../../../../src/plu
import { LicensingPluginSetup } from '../../licensing/public';
import { checkLicense } from '../common/check_license';
import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public';
import { createSavedWorkspacesLoader } from './services/persistence/saved_workspace_loader';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import {
addAppRedirectMessageToUrl,
@ -87,15 +86,7 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies)
}
});
const savedWorkspaceLoader = createSavedWorkspacesLoader({
chrome: deps.coreStart.chrome,
indexPatterns: deps.data.indexPatterns,
overlays: deps.coreStart.overlays,
savedObjectsClient: deps.coreStart.savedObjects.client,
basePath: deps.coreStart.http.basePath,
});
initGraphApp(graphAngularModule, { ...deps, savedWorkspaceLoader });
initGraphApp(graphAngularModule, deps);
const $injector = mountGraphApp(appBasePath, element);
return () => {
licenseSubscription.unsubscribe();

View file

@ -0,0 +1,207 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { cloneDeep, assign, defaults, forOwn } from 'lodash';
import { i18n } from '@kbn/i18n';
import {
IBasePath,
OverlayStart,
SavedObjectsClientContract,
SavedObjectAttributes,
} from 'kibana/public';
import {
SavedObjectSaveOpts,
checkForDuplicateTitle,
saveWithConfirmation,
isErrorNonFatal,
SavedObjectKibanaServices,
} from '../../../../../src/plugins/saved_objects/public';
import {
injectReferences,
extractReferences,
} from '../services/persistence/saved_workspace_references';
import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/public';
import { GraphWorkspaceSavedObject } from '../types';
const savedWorkspaceType = 'graph-workspace';
const mapping: Record<string, string> = {
title: 'text',
description: 'text',
numLinks: 'integer',
numVertices: 'integer',
version: 'integer',
wsState: 'json',
};
const defaultsProps = {
title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', {
defaultMessage: 'New Graph Workspace',
}),
numLinks: 0,
numVertices: 0,
wsState: '{}',
version: 1,
};
const urlFor = (basePath: IBasePath, id: string) =>
basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`);
function mapHits(hit: { id: string; attributes: Record<string, unknown> }, url: string) {
const source = hit.attributes;
source.id = hit.id;
source.url = url;
source.icon = 'fa-share-alt'; // looks like a graph
return source;
}
interface SavedWorkspaceServices {
basePath: IBasePath;
savedObjectsClient: SavedObjectsClientContract;
}
export function findSavedWorkspace(
{ savedObjectsClient, basePath }: SavedWorkspaceServices,
searchString: string,
size: number = 100
) {
return savedObjectsClient
.find<Record<string, unknown>>({
type: savedWorkspaceType,
search: searchString ? `${searchString}*` : undefined,
perPage: size,
searchFields: ['title^3', 'description'],
})
.then(resp => {
return {
total: resp.total,
hits: resp.savedObjects.map(hit => mapHits(hit, urlFor(basePath, hit.id))),
};
});
}
export async function getSavedWorkspace(
savedObjectsClient: SavedObjectsClientContract,
id?: string
) {
const savedObject = {
id,
displayName: 'graph workspace',
getEsType: () => savedWorkspaceType,
} as { [key: string]: any };
if (!id) {
assign(savedObject, defaultsProps);
return Promise.resolve(savedObject);
}
const resp = await savedObjectsClient.get<Record<string, unknown>>(savedWorkspaceType, id);
savedObject._source = cloneDeep(resp.attributes);
if (!resp._version) {
throw new SavedObjectNotFound(savedWorkspaceType, id || '');
}
// assign the defaults to the response
defaults(savedObject._source, defaultsProps);
// transform the source using JSON.parse
if (savedObject._source.wsState) {
savedObject._source.wsState = JSON.parse(savedObject._source.wsState as string);
}
// Give obj all of the values in _source.fields
assign(savedObject, savedObject._source);
savedObject.lastSavedTitle = savedObject.title;
if (resp.references && resp.references.length > 0) {
injectReferences(savedObject, resp.references);
}
return savedObject as GraphWorkspaceSavedObject;
}
export function deleteSavedWorkspace(
savedObjectsClient: SavedObjectsClientContract,
ids: string[]
) {
return Promise.all(ids.map((id: string) => savedObjectsClient.delete(savedWorkspaceType, id)));
}
export async function saveSavedWorkspace(
savedObject: GraphWorkspaceSavedObject,
{
confirmOverwrite = false,
isTitleDuplicateConfirmed = false,
onTitleDuplicate,
}: SavedObjectSaveOpts = {},
services: {
savedObjectsClient: SavedObjectsClientContract;
overlays: OverlayStart;
}
) {
// Save the original id in case the save fails.
const originalId = savedObject.id;
// Read https://github.com/elastic/kibana/issues/9056 and
// https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable
// exists.
// The goal is to move towards a better rename flow, but since our users have been conditioned
// to expect a 'save as' flow during a rename, we are keeping the logic the same until a better
// UI/UX can be worked out.
if (savedObject.copyOnSave) {
delete savedObject.id;
}
let attributes: SavedObjectAttributes = {};
forOwn(mapping, (fieldType, fieldName) => {
const savedObjectFieldVal = savedObject[fieldName as keyof GraphWorkspaceSavedObject] as string;
if (savedObjectFieldVal != null) {
attributes[fieldName as keyof GraphWorkspaceSavedObject] =
fieldName === 'wsState' ? JSON.stringify(savedObjectFieldVal) : savedObjectFieldVal;
}
});
const extractedRefs = extractReferences({ attributes, references: [] });
const references = extractedRefs.references;
attributes = extractedRefs.attributes;
if (!references) {
throw new Error('References not returned from extractReferences');
}
try {
await checkForDuplicateTitle(
savedObject as any,
isTitleDuplicateConfirmed,
onTitleDuplicate,
services as SavedObjectKibanaServices
);
savedObject.isSaving = true;
const createOpt = {
id: savedObject.id,
migrationVersion: savedObject.migrationVersion,
references,
};
const resp = confirmOverwrite
? await saveWithConfirmation(attributes, savedObject, createOpt, services)
: await services.savedObjectsClient.create(savedObject.getEsType(), attributes, {
...createOpt,
overwrite: true,
});
savedObject.id = resp.id;
savedObject.isSaving = false;
savedObject.lastSavedTitle = savedObject.title;
return savedObject.id;
} catch (err) {
savedObject.isSaving = false;
savedObject.id = originalId;
if (isErrorNonFatal(err)) {
return '';
}
return Promise.reject(err);
}
}

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { extractReferences, injectReferences } from './saved_workspace_references';
import {
SavedObject,
createSavedObjectClass,
SavedObjectKibanaServices,
} from '../../../../../../src/plugins/saved_objects/public';
export interface SavedWorkspace extends SavedObject {
wsState?: string;
}
export function createSavedWorkspaceClass(services: SavedObjectKibanaServices) {
// SavedWorkspace constructor. Usually you'd interact with an instance of this.
// ID is option, without it one will be generated on save.
const SavedObjectClass = createSavedObjectClass(services);
class SavedWorkspaceClass extends SavedObjectClass {
public static type: string = 'graph-workspace';
// if type:workspace has no mapping, we push this mapping into ES
public static mapping: Record<string, string> = {
title: 'text',
description: 'text',
numLinks: 'integer',
numVertices: 'integer',
version: 'integer',
wsState: 'json',
};
// Order these fields to the top, the rest are alphabetical
public static fieldOrder = ['title', 'description'];
public static searchSource = false;
public wsState?: string;
constructor(id: string) {
// Gives our SavedWorkspace the properties of a SavedObject
super({
type: SavedWorkspaceClass.type,
mapping: SavedWorkspaceClass.mapping,
searchSource: SavedWorkspaceClass.searchSource,
extractReferences,
injectReferences,
// if this is null/undefined then the SavedObject will be assigned the defaults
id,
// default values that will get assigned if the doc is new
defaults: {
title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', {
defaultMessage: 'New Graph Workspace',
}),
numLinks: 0,
numVertices: 0,
wsState: '{}',
version: 1,
},
});
}
// Overwrite the default getDisplayName function which uses type and which is not very
// user friendly for this object.
getDisplayName = () => {
return 'graph workspace';
};
}
return SavedWorkspaceClass;
}

View file

@ -1,69 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IBasePath } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { SavedObjectKibanaServices } from '../../../../../../src/plugins/saved_objects/public';
import { createSavedWorkspaceClass } from './saved_workspace';
export function createSavedWorkspacesLoader(
services: SavedObjectKibanaServices & { basePath: IBasePath }
) {
const { savedObjectsClient, basePath } = services;
const SavedWorkspace = createSavedWorkspaceClass(services);
const urlFor = (id: string) =>
basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`);
const mapHits = (hit: { id: string; attributes: Record<string, unknown> }) => {
const source = hit.attributes;
source.id = hit.id;
source.url = urlFor(hit.id);
source.icon = 'fa-share-alt'; // looks like a graph
return source;
};
return {
type: SavedWorkspace.type,
Class: SavedWorkspace,
loaderProperties: {
name: 'Graph workspace',
noun: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspaceLabel', {
defaultMessage: 'Graph workspace',
}),
nouns: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspacesLabel', {
defaultMessage: 'Graph workspaces',
}),
},
// Returns a single dashboard by ID, should be the name of the workspace
get: (id: string) => {
// Returns a promise that contains a workspace which is a subclass of docSource
// @ts-ignore
return new SavedWorkspace(id).init();
},
urlFor,
delete: (ids: string | string[]) => {
const idArr = Array.isArray(ids) ? ids : [ids];
return Promise.all(
idArr.map((id: string) => savedObjectsClient.delete(SavedWorkspace.type, id))
);
},
find: (searchString: string, size: number = 100) => {
return savedObjectsClient
.find<Record<string, unknown>>({
type: SavedWorkspace.type,
search: searchString ? `${searchString}*` : undefined,
perPage: size,
searchFields: ['title^3', 'description'],
})
.then(resp => {
return {
total: resp.total,
hits: resp.savedObjects.map(hit => mapHits(hit)),
};
});
},
};
}

View file

@ -5,7 +5,6 @@
*/
import { extractReferences, injectReferences } from './saved_workspace_references';
import { SavedWorkspace } from './saved_workspace';
describe('extractReferences', () => {
test('extracts references from wsState', () => {
@ -67,7 +66,7 @@ describe('injectReferences', () => {
indexPatternRefName: 'indexPattern_0',
bar: true,
}),
} as SavedWorkspace;
};
const references = [
{
name: 'indexPattern_0',
@ -89,7 +88,7 @@ Object {
const context = {
id: '1',
title: 'test',
} as SavedWorkspace;
} as any;
injectReferences(context, []);
expect(context).toMatchInlineSnapshot(`
Object {
@ -103,7 +102,7 @@ Object {
const context = {
id: '1',
wsState: JSON.stringify({ bar: true }),
} as SavedWorkspace;
};
injectReferences(context, []);
expect(context).toMatchInlineSnapshot(`
Object {
@ -119,7 +118,7 @@ Object {
wsState: JSON.stringify({
indexPatternRefName: 'indexPattern_0',
}),
} as SavedWorkspace;
};
expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
`"Could not find reference \\"indexPattern_0\\""`
);

View file

@ -5,7 +5,6 @@
*/
import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public';
import { SavedWorkspace } from './saved_workspace';
export function extractReferences({
attributes,
@ -38,7 +37,10 @@ export function extractReferences({
};
}
export function injectReferences(savedObject: SavedWorkspace, references: SavedObjectReference[]) {
export function injectReferences(
savedObject: { wsState?: string },
references: SavedObjectReference[]
) {
// Skip if wsState is missing, at the time of development of this, there is no guarantee each
// saved object has wsState.
if (typeof savedObject.wsState !== 'string') {

View file

@ -5,18 +5,24 @@
*/
import React from 'react';
import { I18nStart } from 'src/core/public';
import { I18nStart, OverlayStart, SavedObjectsClientContract } from 'src/core/public';
import { SaveResult } from 'src/plugins/saved_objects/public';
import { GraphWorkspaceSavedObject, GraphSavePolicy } from '../types';
import { SaveModal, OnSaveGraphProps } from '../components/save_modal';
export interface SaveWorkspaceServices {
overlays: OverlayStart;
savedObjectsClient: SavedObjectsClientContract;
}
export type SaveWorkspaceHandler = (
saveOptions: {
confirmOverwrite: boolean;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
},
dataConsent: boolean
dataConsent: boolean,
services: SaveWorkspaceServices
) => Promise<SaveResult>;
export function openSaveModal({
@ -26,6 +32,7 @@ export function openSaveModal({
saveWorkspace,
showSaveModal,
I18nContext,
services,
}: {
savePolicy: GraphSavePolicy;
hasData: boolean;
@ -33,6 +40,7 @@ export function openSaveModal({
saveWorkspace: SaveWorkspaceHandler;
showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void;
I18nContext: I18nStart['Context'];
services: SaveWorkspaceServices;
}) {
const currentTitle = workspace.title;
const currentDescription = workspace.description;
@ -52,7 +60,7 @@ export function openSaveModal({
isTitleDuplicateConfirmed,
onTitleDuplicate,
};
return saveWorkspace(saveOptions, dataConsent).then(response => {
return saveWorkspace(saveOptions, dataConsent, services).then(response => {
// If the save wasn't successful, put the original values back.
if (!('id' in response) || !Boolean(response.id)) {
workspace.title = currentTitle;

View file

@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { NotificationsStart, HttpStart } from 'kibana/public';
import {
NotificationsStart,
HttpStart,
OverlayStart,
SavedObjectsClientContract,
} from 'kibana/public';
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware, AnyAction } from 'redux';
import { ChromeStart } from 'kibana/public';
@ -79,6 +84,13 @@ export function createMockGraphStore({
setLiveResponseFields: jest.fn(),
setUrlTemplates: jest.fn(),
setWorkspaceInitialized: jest.fn(),
overlays: ({
openModal: jest.fn(),
} as unknown) as OverlayStart,
savedObjectsClient: ({
find: jest.fn(),
get: jest.fn(),
} as unknown) as SavedObjectsClientContract,
...mockedDepsOverwrites,
};
const sagaMiddleware = createSagaMiddleware();

View file

@ -40,6 +40,10 @@ jest.mock('../services/save_modal', () => ({
openSaveModal: jest.fn(),
}));
jest.mock('../helpers/saved_workspace_utils', () => ({
saveSavedWorkspace: jest.fn().mockResolvedValueOnce('123'),
}));
describe('persistence sagas', () => {
let env: MockedGraphEnvironment;
@ -90,7 +94,6 @@ describe('persistence sagas', () => {
savePolicy: 'configAndDataWithConsent',
},
});
(env.mockedDeps.getSavedWorkspace().save as jest.Mock).mockResolvedValueOnce('123');
env.mockedDeps.getSavedWorkspace().id = '123';
});

View file

@ -22,6 +22,8 @@ import {
import { updateMetaData, metaDataSelector } from './meta_data';
import { openSaveModal, SaveWorkspaceHandler } from '../services/save_modal';
import { getEditPath } from '../services/url';
import { saveSavedWorkspace } from '../helpers/saved_workspace_utils';
const actionCreator = actionCreatorFactory('x-pack/graph');
export const loadSavedWorkspace = actionCreator<GraphWorkspaceSavedObject>('LOAD_WORKSPACE');
@ -140,7 +142,8 @@ function showModal(
) {
const saveWorkspaceHandler: SaveWorkspaceHandler = async (
saveOptions,
userHasConfirmedSaveWorkspaceData
userHasConfirmedSaveWorkspaceData,
services
) => {
const canSaveData =
deps.savePolicy === 'configAndData' ||
@ -157,7 +160,7 @@ function showModal(
canSaveData
);
try {
const id = await savedWorkspace.save(saveOptions);
const id = await saveSavedWorkspace(savedWorkspace, saveOptions, services);
if (id) {
const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', {
defaultMessage: 'Saved "{workspaceTitle}"',
@ -200,5 +203,9 @@ function showModal(
showSaveModal: deps.showSaveModal,
saveWorkspace: saveWorkspaceHandler,
I18nContext: deps.I18nContext,
services: {
savedObjectsClient: deps.savedObjectsClient,
overlays: deps.overlays,
},
});
}

View file

@ -6,7 +6,7 @@
import createSagaMiddleware, { SagaMiddleware } from 'redux-saga';
import { combineReducers, createStore, Store, AnyAction, Dispatch, applyMiddleware } from 'redux';
import { ChromeStart, I18nStart } from 'kibana/public';
import { ChromeStart, I18nStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public';
import { CoreStart } from 'src/core/public';
import {
fieldsReducer,
@ -54,6 +54,8 @@ export interface GraphStoreDependencies {
getSavedWorkspace: () => GraphWorkspaceSavedObject;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
overlays: OverlayStart;
savedObjectsClient: SavedObjectsClientContract;
showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void;
savePolicy: GraphSavePolicy;
changeUrl: (newUrl: string) => void;

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObject } from '../../../../../src/plugins/saved_objects/public';
import { AdvancedSettings, UrlTemplate, WorkspaceField } from './app_state';
import { WorkspaceNode, WorkspaceEdge } from './workspace_state';
@ -12,15 +11,23 @@ type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
/**
* Workspace fetched from server.
* This type is returned by `SavedWorkspacesProvider#get`.
*/
export interface GraphWorkspaceSavedObject extends SavedObject {
title: string;
export interface GraphWorkspaceSavedObject {
copyOnSave?: boolean;
description: string;
displayName: string;
getEsType(): string;
id?: string;
isSaving?: boolean;
lastSavedTitle?: string;
migrationVersion?: Record<string, any>;
numLinks: number;
numVertices: number;
version: number;
title: string;
type: string;
version?: number;
wsState: string;
_source: Record<string, unknown>;
}
export interface SerializedWorkspaceState {

View file

@ -5701,8 +5701,6 @@
"xpack.graph.outlinkEncoders.textPlainTitle": "プレインテキスト",
"xpack.graph.pluginDescription": "Elasticsearch データの関連性のある関係を浮上させ分析します。",
"xpack.graph.savedWorkspace.workspaceNameTitle": "新規グラフワークスペース",
"xpack.graph.savedWorkspaces.graphWorkspaceLabel": "グラフワークスペース",
"xpack.graph.savedWorkspaces.graphWorkspacesLabel": "グラフワークスペース",
"xpack.graph.saveWorkspace.savingErrorMessage": "ワークスペースの保存に失敗しました: {message}",
"xpack.graph.saveWorkspace.successNotification.noDataSavedText": "構成が保存されましたが、データは保存されませんでした",
"xpack.graph.saveWorkspace.successNotificationTitle": "「{workspaceTitle}」が保存されました",

View file

@ -5701,8 +5701,6 @@
"xpack.graph.outlinkEncoders.textPlainTitle": "纯文本",
"xpack.graph.pluginDescription": "显示并分析 Elasticsearch 数据中的相关关系。",
"xpack.graph.savedWorkspace.workspaceNameTitle": "新建 Graph 工作空间",
"xpack.graph.savedWorkspaces.graphWorkspaceLabel": "Graph 工作空间",
"xpack.graph.savedWorkspaces.graphWorkspacesLabel": "Graph 工作空间",
"xpack.graph.saveWorkspace.savingErrorMessage": "无法保存工作空间:{message}",
"xpack.graph.saveWorkspace.successNotification.noDataSavedText": "配置会被保存,但不保存数据",
"xpack.graph.saveWorkspace.successNotificationTitle": "已保存“{workspaceTitle}”",