mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* 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:
parent
7c4c3b165c
commit
8edfd14da5
20 changed files with 478 additions and 180 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
207
x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts
Normal file
207
x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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\\""`
|
||||
);
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}」が保存されました",
|
||||
|
|
|
@ -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}”",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue