正在加载

共享保存对象

本指南介绍了“共享保存对象”的努力方向,以及插件开发者需要注意的 Kibana 计划于 8.0 版本发布的重大更改。它还介绍了开发者如何利用此功能。

从 8.7.0 开始,作为零停机升级的一个步骤,插件不再允许更新现有的单空间保存对象类型以使其可共享。 请注意,仍然可以将新的保存对象类型定义为 'multiple''multiple-isolated'

保存对象(以下简称“对象”)用于在 Kibana 中存储各种内容,从仪表板到索引模式到机器学习作业。使对象可共享的努力可以用一张图来概括

Sharing Saved Objects overview

每个插件都可以注册不同的对象类型以在 Kibana 中使用。 历史上,对象可以是隔离的(存在于单个 空间中)或全局的(存在于所有空间中),两者之间没有其他类型。 从 7.12 版本开始,Kibana 现在支持两种额外的对象类型

存在位置 对象 ID 注册为
全局 所有空间 全局唯一 namespaceType: 'agnostic'
隔离 1 个空间 每个空间中唯一 namespaceType: 'single'
(新)具有共享能力 1 个空间 全局唯一 namespaceType: 'multiple-isolated'
(新)可共享 1 个或多个空间 全局唯一 namespaceType: 'multiple'

理想情况下,Kibana 中大多数类型的对象最终都将是可共享的;但是,我们还引入了 具有共享能力的对象,作为插件开发者完全支持此功能的垫脚石。

实现可共享的保存对象类型分两个阶段完成

  • 阶段 1:将现有的隔离对象类型转换为具有共享能力的对象类型。 继续阅读!
  • 阶段 2:将现有的具有共享能力的对象类型转换为可共享的对象类型,创建新的可共享对象类型。 跳转到阶段 2 开发人员流程图

为了实现此功能,我们必须对对象序列化为原始 Elasticsearch 文档的方式进行关键更改。 因此,需要更改一些现有对象 ID,这将对消费者(插件开发者)与对象交互的方式造成一些重大更改。 我们已经实施了缓解措施,因此如果消费者按照以下步骤进行操作,这些更改将不会影响最终用户。

现有的隔离对象类型需要经过特殊的转换过程,才能在将 Kibana 升级到 8.0 版本后变成具有共享能力的对象类型。 一旦对象被转换,就可以很容易地在任何未来的版本中切换为完全可共享。 这种转换会改变任何不在默认空间中的现有对象的 ID。 更改对象 ID 本身会产生几个连锁反应

  • 指向其他对象的非标准链接可能会中断 - 通过 步骤 1 缓解
  • 指向对象的“深度链接”页面 (URL) 可能会中断 - 通过 步骤 2步骤 3 缓解
  • 加密对象可能无法解密 - 通过 步骤 5 缓解

要非常清楚地说:只有您按照以下步骤进行操作,才能缓解所有这些影响!

提示

外部插件也可以转换其对象,但他们不必在 8.0 版本发布之前这样做

如果您仍在阅读此页面,您可能正在开发一个注册对象类型的 Kibana 插件,并且您想知道需要采取哪些步骤来为 8.0 版本做准备并减轻任何重大更改! 根据您使用保存对象的方式,您可能需要采取最多 5 个步骤,这些步骤将在下面的单独部分中详细说明。 请参阅此流程图

Sharing Saved Objects phase 1 - developer flowchart

提示

有一个概念验证 (POC) 拉取请求来演示这些更改。 它首先添加一个简单的测试插件,允许用户创建和查看笔记。 然后,它会按照流程图中的步骤将隔离的“note”对象转换为具有共享能力的对象。 当您阅读本指南时,您可以在 POC 中进行跟踪,以确切了解如何执行这些步骤。

[待确认:引用] 如果您的对象存储任何指向其他对象的链接(带有对象类型/ID),您需要采取特定的步骤来确保这些链接在 8.0 升级后继续起作用。

⚠️ 此步骤必须不晚于 7.16 版本完成。 ⚠️

[待确认:引用] 如果您对问题 1的回答是“是”,则需要确保您的对象链接存储在根级别的 references 字段中。 当给定对象的 ID 发生更改时,此字段将为其他对象进行相应更新。

下图显示了从“case”对象到“action”对象的两种不同的对象链接示例。 顶部显示了链接到另一个对象的不正确方式,底部显示了正确的方式。

Sharing Saved Objects step 1

如果您的对象未使用根级别的 references 字段,则需要在8.0 版本发布之前添加迁移来修复它。 这是上面示例的迁移函数

function migrateCaseToV716(
  doc: SavedObjectUnsanitizedDoc<{ connector: { type: string; id: string } }>
): SavedObjectSanitizedDoc<unknown> {
  const {
    connector: { type: connectorType, id: connectorId, ...otherConnectorAttrs },
  } = doc.attributes;
  const { references = [] } = doc;
  return {
    ...doc,
    attributes: {
      ...doc.attributes,
      connector: otherConnectorAttrs,
    },
    references: [...references, { type: connectorType, id: connectorId, name: 'connector' }],
  };
}

...

// Use this migration function where the "case" object type is registered
migrations: {
  '7.16.0': migrateCaseToV716,
},
注意

提醒一下,不要忘记添加单元测试和集成测试!

[待确认:引用] 深度链接是指向显示特定对象的页面的 URL。 最终用户可以为这些 URL 添加书签或安排报告,因此至关重要的是要确保这些 URL 继续有效。 下图显示了指向 Canvas workpad 对象的深度链接示例

Sharing Saved Objects deep link example

请注意,某些 URL 可能包含指向多个对象的深度链接,例如,仪表板索引模式的过滤器。

⚠️ 此步骤最好在 7.16 版本中完成;它必须不晚于 8.0 版本完成。 ⚠️

[待确认:引用] 如果您对问题 2的回答是“是”,则需要确保在使用 SavedObjectsClient 获取具有其 ID 的对象时,使用不同的 API 来执行此操作。 现有的 get() 函数只会使用其当前 ID 查找对象。 为了确保您现有的深度链接 URL 不会中断,您应该使用新的 resolve() 函数;这会尝试使用其旧 ID其当前 ID 查找对象

简而言之,如果您的深度链接页面之前有类似的内容

const savedObject = savedObjectsClient.get(objType, objId);

您需要将其更改为此

const resolveResult = savedObjectsClient.resolve(objType, objId);
const savedObject = resolveResult.saved_object;
提示

请参阅 POC 的第 2 步 中的示例!

SavedObjectsResolveResponse 接口有四个字段,总结如下

  • saved_object - 找到的保存对象。
  • outcome - 以下值之一:'exactMatch' | 'aliasMatch' | 'conflict'
  • alias_target_id - 如果结果是 'aliasMatch''conflict',则会定义此值。 这意味着具有此 ID 的旧版 URL 别名指向具有不同 ID 的对象。
  • alias_purpose - 如果结果是 'aliasMatch''conflict',则会定义此值。 它描述了创建旧版 URL 别名的原因。

SavedObjectsClient 在服务器端和客户端都可用。 您可以通过自定义 HTTP 路由在服务器端获取对象,也可以直接在客户端获取对象。 无论哪种方式,都需要将 outcomealias_target_id 字段传递到您的客户端代码,并且您应该在下一步中相应地更新您的 UI。

注意

您不需要在任何地方都使用 resolve()您应该只将它用于深度链接

⚠️ 此步骤最好在 7.16 版本中完成;它必须不晚于 8.0 版本完成。 ⚠️

[待确认:引用] Spaces 插件 API 公开了 React 组件和函数,您应该使用它们以一致的方式为最终用户呈现您的 UI。 您的 UI 需要使用 Core HTTP 服务和 Spaces 插件 API 来执行此操作。

您的页面应该根据结果进行更改

Sharing Saved Objects resolve outcomes overview

提示

请参阅 POC 的第 3 步 中的示例!

  1. 更新插件的 kibana.json 以添加对 Spaces 插件的依赖项

    ...
    "optionalPlugins": ["spaces"]
    
  2. 更新插件的 tsconfig.json 以添加对 Space 插件的类型定义的依赖项

    ...
    "references": [
      ...
      { "path": "../spaces/tsconfig.json" },
    ]
    
  3. 更新您的 Plugin 类实现以依赖于 Spaces 插件 API

    interface PluginStartDeps {
      spaces?: SpacesPluginStart;
    }
    
    export class MyPlugin implements Plugin<{}, {}, {}, PluginStartDeps> {
      public setup(core: CoreSetup<PluginStartDeps>) {
        core.application.register({
          ...
          async mount(appMountParams: AppMountParameters) {
            const [, pluginStartDeps] = await core.getStartServices();
            const { spaces: spacesApi } = pluginStartDeps;
            ...
            // pass `spacesApi` to your app when you render it
          },
        });
        ...
      }
    }
    
  4. 在您的深度链接页面中,添加对 'aliasMatch' 结果的检查

    if (spacesApi && resolveResult.outcome === 'aliasMatch') {
      // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash
      const newObjectId = resolveResult.alias_target_id!;
      // This is always defined if outcome === 'aliasMatch'
      const newPath = `/this/page/${newObjectId}${window.location.hash}`;
      // Use the *local* path within this app (do not include the "/app/appId" prefix)
      await spacesApi.ui.redirectLegacyUrl({
        path: newPath,
        aliasPurpose: resolveResult.alias_purpose,
        objectNoun: OBJECT_NOUN
      });
      return;
    }
    
    1. 自 8.2 起,aliasPurpose 字段是必需的,因为 API 响应现在包含创建别名的原因,以告知客户端是否应显示 toast。
    2. objectNoun 字段是可选的。 它只是将 toast 中的“object”更改为您指定的任何内容 — 您可能希望 toast 显示“dashboard”或“data view”而不是“object”。
  5. 最后,在您的深度链接页面中,添加一个函数,以便在出现 'conflict' 结果时创建一个标注。

    const getLegacyUrlConflictCallout = () => {
      // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
      if (spacesApi && resolveResult.outcome === 'conflict') {
        // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
        // callout with a warning for the user, and provide a way for them to navigate to the other object.
        const currentObjectId = savedObject.id;
        const otherObjectId = resolveResult.alias_target_id!;
        const otherObjectPath = `/this/page/${otherObjectId}${window.location.hash}`; // Use the *local* path within this app (do not include the "/app/appId" prefix)
        return (
          <>
            {spacesApi.ui.components.getLegacyUrlConflict({
              objectNoun: OBJECT_NOUN,
              currentObjectId,
              otherObjectId,
              otherObjectPath,
            })}
            <EuiSpacer />
          </>
        );
      }
      return null;
    };
    ...
    return (
      <EuiPage>
        <EuiPageBody>
          <EuiPageSection>
            {/* If we have a legacy URL conflict callout to display, show it at the top of the page */}
            {getLegacyUrlConflictCallout()}
            <EuiPageHeader>
    ...
    );
    
    1. 如果 outcome === 'conflict',则始终定义此项。
  6. 生成临时数据并使用不同的结果测试您页面的行为。

注意

提醒一下,不要忘记添加单元测试和功能测试!

⚠️ 必须在 8.0 版本中完成此步骤(不能早于或晚于)。⚠️

[待定:引用] 完成步骤 3后,您可以添加代码来转换您的对象。

警告

之前的步骤可以向后移植到 7.x 分支,但是此步骤(转换本身)只能在 8.0 中进行!您应该为此使用单独的拉取请求。

注册对象时,您需要更改 namespaceType,并添加 convertToMultiNamespaceTypeVersion 字段。 此特殊字段将触发在用户安装 Kibana 8.0 版本期间,Core 迁移升级过程中发生的实际转换。

Sharing Saved Objects conversion code

提示

请参阅POC 的步骤 4中的示例!

注意

提醒一下,不要忘记添加集成测试!

[待定:引用] 保存的对象可以选择使用加密保存的对象插件进行加密。 很少有对象类型被加密,因此大多数插件开发人员都不会受到影响。

⚠️ 必须在 8.0 版本中完成此步骤(不能早于或晚于)。⚠️

[待定:引用] 如果您对问题 3回答“是”,则需要采取其他步骤以确保您的对象在转换过程后仍可以解密。 加密的保存对象使用一些字段作为“附加身份验证数据”(AAD)的一部分,以防御不同类型的加密攻击。 对象 ID 是此 AAD 的一部分,因此在更改对象的 ID 之后,将无法使用标准流程解密该对象。

为了缓解这个问题,您需要添加一个“no-op” ESO 迁移,该迁移将在 8.0 升级过程中转换对象后立即应用。 这将使用其旧 ID 解密对象,然后使用其新 ID 重新加密它。

Sharing Saved Objects ESO migration

注意

提醒一下,不要忘记添加单元测试和集成测试!

本节介绍如何将支持共享的对象类型切换为可共享的对象类型创建新的可共享的已保存对象类型。 请参阅此流程图。

Sharing Saved Objects phase 2 - developer flowchart

[待定:引用] 注册对象时,需要设置正确的 namespaceType。 如果您有现有的“支持共享”的对象类型,则只需更改它即可。

Sharing Saved Objects registration (shareable)

[待定:引用] 如果一个对象被共享到多个空间,则必须使用force 删除选项才能删除它。您应该始终知道已保存的对象是否存在于多个空间中,并且在这种情况下应警告用户。

如果您的 UI 允许用户删除您的对象,您可以定义如下警告消息

const { namespaces, id } = savedObject;
const warningMessage =
  namespaces.length > 1 || namespaces.includes('*') ? (
    <FormattedMessage
      id="myPlugin.deleteObjectWarning"
      defaultMessage="When you delete this object, you remove it from every space it is shared in. You can't undo this action."
    />
  ) : null;

数据视图页面位于Stack Management 中,它使用了一种类似的方法在其删除确认模态框中显示警告

Sharing Saved Objects deletion warning

[待定:引用] 用户将需要一种查看您的对象当前分配给哪些空间以及将其共享到其他空间的方法。 您可以通过两种方式完成此操作,并且许多消费者都希望同时实现这两种方式。

  1. (强烈建议)将可重用组件添加到您的应用程序,使其“空间感知”。 与空间相关的组件由 spaces 插件导出,您可以在自己的应用程序中使用它们。

    首先,请确保您的页面内容包含在spaces context provider中。

    const ContextWrapper = useMemo(
      () =>
        spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
      [spacesApi]
    );
    
    ...
    
    return (
      <ContextWrapper feature='my-feature-id'>
        <!-- your page contents here -->
      </ContextWrapper>
    );
    

    其次,显示对象的空间列表;第三,显示一个悬浮窗,供用户编辑对象分配的空间。 您可能需要遵循数据视图页面的示例,并将它们合并为一个组件,以便可以单击空间列表以显示悬浮窗。

    const [showFlyout, setShowFlyout] = useState(false);
    const LazySpaceList = useCallback(spacesApi.ui.components.getSpaceList, [spacesApi]);
    const LazyShareToSpaceFlyout = useCallback(spacesApi.ui.components.getShareToSpaceFlyout, [spacesApi]);
    
    const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
      savedObjectTarget: {
        type: myObject.type,
        namespaces: myObject.namespaces,
        id: myObject.id,
        icon: 'beaker',
        title: myObject.attributes.title,
        noun: OBJECT_NOUN,
      },
      onUpdate: () => { /* callback when the object is updated */ },
      onClose: () => setShowFlyout(false),
    };
    
    const canAssignSpaces = !capabilities || !!capabilities.savedObjectsManagement.shareIntoSpace;
    const clickProperties = canAssignSpaces
      ? { cursorStyle: 'pointer', listOnClick: () => setShowFlyout(true) }
      : { cursorStyle: 'not-allowed' };
    return (
      <>
        <LazySpaceList
          namespaces={spaceIds}
          displayLimit={8}
          behaviorContext="outside-space"
          {...clickProperties}
        />
        {showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
      </>
    );
    
    1. icon 字段是可选的。 它指定一个EUI 图标类型,该类型将显示在悬浮窗标题中。
    2. title 字段是可选的。 它为您的对象指定一个人性化的标识符,该标识符将显示在悬浮窗标题中。
    3. noun 字段是可选的。 它只是将悬浮窗中的“对象”更改为您指定的任何内容,您可能希望悬浮窗显示“仪表板”或“数据视图”。
    4. behaviorContext 字段是可选的。 它控制空间的显示方式。 使用 "outside-space" 行为上下文时,空间列表在任何特定空间之外呈现,因此活动空间包含在列表中。 另一方面,当使用 "within-space" 行为上下文时,空间列表在活动空间内呈现,因此活动空间将从列表中排除。
  2. 允许用户在 Stack Management 中的Saved Objects Management page中访问您的对象。 您可以通过确保您的对象在您的保存对象类型注册中标记为可导入和导出来实现。

    name: 'my-object-type',
    management: {
      isImportableAndExportable: true,
    },
    ...
    

    如果这样做,您的对象将显示在保存的对象管理页面中,用户可以在其中将它们分配给多个空间。

我们实现了“支持共享”对象类型,作为消费者当前拥有孤立对象,但尚未准备好支持完全可共享对象的中间步骤。 这主要是因为我们要确保所有对象类型在 8.0 版本中同时转换,以最大程度地减少最终用户体验的混乱和中断。

我们意识到,对于某些 Kibana 团队来说,转换过程及其所有过程可能需要在 8.0 版本之前做好准备,这是一项不小的工作。 只要使对象“支持共享”,就可以确保其 ID 在全局范围内是唯一的,因此以后在适当的时候使该对象“可共享”将变得很简单。

开发人员可以轻松地翻转开关以将“支持共享”的对象变为“可共享”的对象,因为它们都以相同的方式进行序列化。 但是,我们设想每个消费者都需要制定自己的计划并在使对象“可共享”时进行其他 UI 更改。 例如,某些用户可能无权访问“保存的对象管理”页面,但是我们仍然希望这些用户能够查看其对象存在的空间并将其共享到其他空间。 每个应用程序都应添加适当的 UI 控件来处理此问题。

这是因为孤立对象被序列化为原始 Elasticsearch 文档的方式。 今天,每个原始文档 ID 都包含其空间 ID(namespace)作为前缀。 当对象被复制或导入到其他空间时,它们会保留相同的对象 ID,只是在序列化为 Elasticsearch 时具有不同的前缀。 这导致许多 Kibana 安装在不同空间中具有相同的对象 ID 的已保存对象。

Sharing Saved Objects object ID diagram (before conversion)

对象转换后,我们需要删除此前缀。 由于我们的迁移过程存在局限性,因此我们无法主动检查这是否会导致冲突。 因此,我们决定抢先为非默认空间中的每个对象重新生成对象 ID,以确保每个对象 ID 都成为全局唯一的。

Sharing Saved Objects object ID diagram (after conversion)

问题 2中所述,某些 URL 可能包含多个对象 ID,从而有效地深度链接到多个对象。 这些应由插件所有者酌情逐个处理。 一个很好的经验法则是:

  • 页面上的“主要”对象应始终按照步骤 3中的描述处理三种 resolve() 结果。

  • 页面上的任何“辅助”对象可以不同地处理结果。 如果辅助对象 ID 不重要(例如,它仅用作页面锚点),则忽略不同的结果可能更有意义。 如果辅助对象重要的,但在 UI 中没有直接表示,则在遇到 'conflict' 结果时抛出描述性错误可能更有意义。

    • Embeddables 应该使用 spacesApi.ui.components.getEmbeddableLegacyUrlConflict 来渲染冲突错误。

Sharing Saved Objects embeddable legacy URL conflict

查看详细信息会向用户展示如何禁用别名并使用 _disable_legacy_url_aliases API 解决该问题。

Sharing Saved Objects embeddable legacy URL conflict (showing details)

* 如果辅助对象由外部服务(例如索引模式服务)解析,则该服务应仅向消费者提供完整的 outcome。

理想情况下,如果深度链接页面上的辅助对象解析为 'aliasMatch' 结果,则消费者应将用户重定向到具有新 ID 的 URL 并显示 toast 消息。 原因是,我们不希望用户比必要时更频繁地依赖旧 URL 别名。 但是,对于辅助对象的这种处理方式并未被认为是 8.0 版本的关键。

如上图所示,当一个对象被转换为“支持共享”时,如果它存在于非默认空间中,则其 ID 将被更改。 为了保留其旧 ID,我们还创建了一个名为旧 URL 别名(简称“别名”)的特殊对象;此别名保留了目标对象的旧 ID(sourceId),并且包含指向目标对象的新 ID(targetId)的指针。

从设计上讲,别名对最终用户来说几乎是不可见的。 没有 UI 可以直接管理它们。 我们的愿景是,别名将被用作一个临时措施,以帮助我们完成 8.0 的升级过程,但我们将引导用户不要依赖别名,以便我们最终可以弃用并删除它们。

resolve() 函数既检查是否具有给定 ID 的对象存在,检查对象是否具有给定 ID 的别名。

  1. 如果只有前者为真,则结果为 'exactMatch',我们找到了要寻找的精确对象。
  2. 如果只有后者为真,则结果为 'aliasMatch',我们找到了具有此 ID 的别名,该别名将我们指向具有不同 ID 的对象。
  3. 最后,如果两个条件都为真,则结果为 'conflict',我们使用此 ID 找到了两个对象。 为了可用性,我们决定返回最正确的匹配,即精确匹配,而不是在这种情况下返回错误。 通过通知消费者这是一个冲突,消费者可以向最终用户渲染适当的 UI,以解释这可能不是他们实际要寻找的对象。

结果 1

当您使用其当前 ID 解析对象时,结果为 'exactMatch'

Sharing Saved Objects resolve outcome 1 (exactMatch)

这可能发生在默认空间非默认空间中。

结果 2

当你使用旧 ID(其别名的 ID)解析一个对象时,结果是 'aliasMatch'

Sharing Saved Objects resolve outcome 2 (aliasMatch)

此结果只能发生在非默认空间中。

结果 3

第三种结果是一种边缘情况,是其他情况的组合。 如果你解析一个对象 ID,并且找到两个对象——一个完全匹配,另一个作为别名匹配——那么结果是 'conflict'

Sharing Saved Objects resolve outcome 3 (conflict)

实际上,我们有控制措施来防止在你共享、导入或复制对象时发生这种情况。但是,如果以某种方式创建对象,或者用户篡改对象的原始 ES 文档,这种情况仍然可能发生在几种不同的情况下。 既然我们不能 100% 排除这种情况,我们必须优雅地处理它,但我们预计这是一种罕见的发生。

重要的是要注意,当发生 'conflict' 时,返回的对象是“最正确”的匹配项——ID 完全匹配的那一个。

阅读本指南后,你可能会认为使用 resolve() 在任何地方都比使用 get() 更安全或更好。 实际上,我们做出了明确的设计决策,添加了一个单独的 resolve() 函数,因为我们希望限制旧 URL 别名的影响和依赖。 为此,我们根据 resolve() 的使用次数以及遇到的不同结果收集匿名使用数据。 如果 resolve() 的使用频率超过了必要频率,那么该使用数据的作用就会降低。

最终,resolve() 应该用于涉及用户控制的指向对象的深度链接的数据流。 没有理由更改任何其他数据流以使用 resolve()

外部插件(那些未随 Kibana 一起提供的插件)可以使用本指南将任何孤立的对象转换为可共享或完全可共享! 如果你是外部插件开发人员,步骤是相同的,但你无需担心在特定版本之前完成任何事情。 你唯一需要知道的是,你的插件在 8.0 版本之前无法转换你的对象。

有关用户应如何受到影响的更多详细信息,请参阅 已保存对象 ID 文档。

© . All rights reserved.