共享已保存的对象
编辑共享已保存的对象编辑
本指南介绍了“共享已保存的对象”工作,以及插件开发者需要了解的计划在 Kibana 8.0 版本中发布的重大更改。它还介绍了开发者如何利用此功能。
从 8.7.0 版本开始,作为实现*零停机升级*的一个步骤,插件不再允许更新现有的单空间已保存对象类型以使其可共享。请注意,新的已保存对象类型仍然可以定义为 'multiple'
或 'multiple-isolated'
。
概述编辑
已保存的对象(以下简称“对象”)用于在 Kibana 中存储各种内容,从仪表板到索引模式再到机器学习作业。使对象可共享的工作可以概括为一张图片
每个插件都可以注册要在 Kibana 中使用的不同对象类型。过去,对象可以是*隔离的*(存在于单个 空间 中)或*全局的*(存在于所有空间中),没有中间状态。从 7.12 版本开始,Kibana 现在支持另外两种类型的对象
存在位置 |
对象 ID |
注册为 |
|
全局 |
所有空间 |
全局唯一 |
|
隔离 |
1 个空间 |
在每个空间中唯一 |
|
(新增)可共享 |
1 个空间 |
全局唯一 |
|
(新增)共享 |
1 个或多个空间 |
全局唯一 |
|
理想情况下,Kibana 中的大多数类型的对象最终都将是*可共享的*;但是,我们还引入了 *可共享*对象 作为插件开发者完全支持此功能的垫脚石。
实现可共享的已保存对象类型分为两个阶段
- 阶段 1:将现有的隔离对象类型转换为可共享的对象类型。继续阅读!
- 阶段 2:将现有的可共享对象类型切换为可共享的对象类型,*或*创建新的可共享对象类型。跳转到 阶段 2 开发者流程图!
重大更改编辑
为了实现此功能,我们必须对如何将对象序列化为原始 Elasticsearch 文档进行关键更改。因此,需要更改一些现有的对象 ID,这将对使用者(插件开发者)与对象交互的方式造成一些重大更改。我们已经实施了缓解措施,以便 *如果*使用者执行了以下必需的步骤,这些更改将不会影响最终用户。
现有的、隔离的对象类型需要经过特殊的*转换过程*才能在将 Kibana 升级到 8.0 版本后变为可共享的。一旦对象被转换,它们就可以在未来的任何版本中轻松切换为完全可共享的。此转换将更改不在默认空间中的任何现有对象的 ID。更改对象 ID 本身会产生一些连锁反应
需要明确的是:*当且仅当*您按照以下步骤操作时,这些影响才会得到缓解!
外部插件也可以转换其对象,但 它们不必在 8.0 版本之前这样做。
阶段 1 开发者流程图编辑
如果您仍在阅读此页面,您可能正在开发一个注册了对象类型的 Kibana 插件,并且您想知道需要采取哪些步骤来为 8.0 版本做好准备并缓解任何重大更改!根据您使用已保存对象的方式,您可能需要执行多达 5 个步骤,这些步骤将在下面的单独部分中详细介绍。请参考此流程图
有一个概念验证 (POC) 拉取请求来演示这些更改。它首先添加了一个简单的测试插件,允许用户创建和查看笔记。然后,它按照流程图的步骤将隔离的“笔记”对象转换为可共享的对象。在您阅读本指南时,您可以 在 POC 中进行跟踪,以确切了解如何执行这些步骤。
步骤 1编辑
⚠️ 此步骤 必须 在 7.16 版本之前完成。 ⚠️
确保所有对象链接都使用根级别的
references
字段
如果您对 问题 1 的回答是“是”,则需要确保您的对象链接*仅*存储在根级别的 references
字段中。当给定对象的 ID 更改时,此字段将针对其他对象进行相应更新。
下图显示了从“案例”对象到“操作”对象的两种不同对象链接示例。顶部显示了链接到另一个对象的错误方式,底部显示了正确的方式。
如果您的对象*没有*使用根级别的 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, },
提醒:不要忘记添加单元测试和集成测试!
问题 2编辑
是否有任何指向这些对象的“深层链接”?
深层链接是指向显示特定对象的页面的 URL。最终用户可能会将这些 URL 添加到书签或使用它们安排报告,因此确保这些 URL 继续有效至关重要。下图显示了指向 Canvas 工作区对象的一个深层链接示例
请注意,某些 URL 可能包含 指向多个对象的深层链接,例如,一个仪表板*和*一个索引模式的过滤器。
步骤 2编辑
⚠️ 此步骤最好在 7.16 版本中完成;它 必须 在 8.0 版本之前完成。 ⚠️
更新您的代码以使用新的 SavedObjectsClient
resolve()
方法而不是get()
如果您对 问题 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 路由在服务器端获取对象,也可以直接在客户端获取对象。无论哪种方式,都需要将 outcome
和 alias_target_id
字段传递给您的客户端代码,并且您应该在下一步中相应地更新您的 UI。
您不需要在所有地方都使用 resolve()
,您应该只将它用于深层链接!
步骤 3编辑
⚠️ 此步骤最好在 7.16 版本中完成;它 必须 在 8.0 版本之前完成。 ⚠️
更新您的客户端代码以正确处理三种不同的
resolve()
结果
Spaces 插件 API 公开了 React 组件和函数,您应该使用它们以一致的方式为最终用户呈现您的 UI。您的 UI 将需要使用 Core HTTP 服务和 Spaces 插件 API 来执行此操作。
您的页面应该根据结果进行更改
请参阅POC 的步骤 3中的示例!
-
更新您的插件的
kibana.json
以添加对 Spaces 插件的依赖项... "optionalPlugins": ["spaces"]
-
更新您的插件的
tsconfig.json
以添加对 Space 插件类型定义的依赖项... "references": [ ... { "path": "../spaces/tsconfig.json" }, ]
-
更新您的插件类实现以依赖于 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 }, }); ... } }
-
在您的深层链接页面中,添加对
'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; }
-
最后,在您的深层链接页面中,添加一个函数,在出现
'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!; // This is always defined if outcome === 'conflict' 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> ... );
- 生成阶段数据并使用不同的结果测试页面的行为。
提醒:不要忘记添加单元测试和功能测试!
步骤 4编辑
⚠️ 此步骤必须在 8.0 版本中完成(不能更早也不能更晚)。 ⚠️
更新您的服务器端代码以将这些对象转换为“可共享”
步骤 3完成后,您可以添加代码来转换您的对象。
前面的步骤可以反向移植到 7.x 分支,但此步骤(转换本身)只能在 8.0 中进行!您应该为此使用单独的拉取请求。
注册对象时,您需要更改 namespaceType
并添加 convertToMultiNamespaceTypeVersion
字段。当用户安装 Kibana 8.0 版本时,此特殊字段将触发将在 Core 迁移升级过程中发生的实际转换
请参阅POC 的步骤 4中的示例!
提醒:不要忘记添加集成测试!
步骤 5编辑
⚠️ 此步骤必须在 8.0 版本中完成(不能更早也不能更晚)。 ⚠️
更新您的服务器端代码以添加针对这些对象的加密保存对象 (ESO) 迁移
如果您对问题 3的回答是“是”,则需要采取其他步骤以确保在转换过程后仍然可以解密您的对象。加密保存的对象使用某些字段作为“额外身份验证数据”(AAD) 的一部分,以防御不同类型的加密攻击。对象 ID 是此 AAD 的一部分,因此在更改对象的 ID 后,将无法使用标准流程解密该对象。
为了缓解这种情况,您需要添加一个“无操作”ESO 迁移,该迁移将在 8.0 升级过程中转换对象后立即应用。这将使用其旧 ID 解密对象,然后使用其新 ID 重新加密它
提醒:不要忘记添加单元测试和集成测试!
阶段 2 开发人员流程图编辑
本节介绍将可共享对象类型切换为可共享对象类型或创建新的可共享保存对象类型。请参考此流程图
步骤 7编辑
更新保存的对象删除 API 使用情况以处理多个空间
如果一个对象被共享到多个空间,则必须使用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;
数据视图页面在堆栈管理中使用类似的方法在其删除确认模式中显示警告
步骤 8编辑
允许用户查看和更改对象的分配空间
用户需要一种方法来查看您的对象当前分配给哪些空间,并将它们共享到其他空间。您可以通过两种方式实现这一点,许多消费者希望同时实现这两种方式
-
(强烈推荐)将可重用组件添加到您的应用程序中,使其“空间感知”。与空间相关的组件由 spaces 插件导出,您可以在自己的应用程序中使用它们。
首先,确保您的页面内容包含在空间上下文提供程序中
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} />} </> );
icon
字段是可选的。它指定将在弹出窗口标题中显示的EUI 图标类型。title
字段是可选的。它为您的对象指定了一个人类可读的标识符,该标识符将显示在弹出窗口标题中。noun
字段是可选的。它只是将弹出窗口中的“对象”更改为您指定的任何内容 - 您可能希望弹出窗口显示“仪表板”或“数据视图”。behaviorContext
字段是可选的。它控制空间列表的显示方式。使用"outside-space"
行为上下文时,空间列表将在任何特定空间之外呈现,因此活动空间包含在列表中。另一方面,使用"within-space"
行为上下文时,空间列表将在活动空间内呈现,因此活动空间从列表中排除。 -
允许用户在堆栈管理的保存的对象管理页面中访问您的对象。您可以通过确保在您的保存的对象类型注册中将您的对象标记为可导入和可导出来做到这一点
name: 'my-object-type', management: { isImportableAndExportable: true, }, ...
如果这样做,您的对象将在保存的对象管理页面中可见,用户可以在其中将它们分配给多个空间。
常见问题解答 (FAQ)编辑
1. 为什么同时存在“可共享”和“可共享”对象类型?编辑
我们将可共享对象类型作为中间步骤实现,供当前拥有隔离对象但尚未准备好支持完全可共享对象的使用者使用。这主要是因为我们希望确保所有对象类型都在 8.0 版本中同时转换,以最大程度地减少最终用户体验的混乱和中断。
我们意识到,对于一些 Kibana 团队来说,转换过程及其所涉及的一切工作量可能非常大,需要在 8.0 版本发布之前做好准备。只要对象具有可共享性,就能确保其 ID 在全局范围内是唯一的,因此在适当的时候将该对象设置为可共享将变得非常简单。
开发人员可以轻松地通过切换开关将可共享对象转换为可共享对象,因为这两者的序列化方式相同。但是,我们设想每个使用者在将对象设置为可共享时都需要制定自己的计划并进行额外的 UI 更改。例如,某些用户可能无权访问“已保存对象管理”页面,但我们仍然希望这些用户能够查看其对象所在的空间并将其共享到其他空间。每个应用程序都应添加适当的 UI 控件来处理此问题。
这是因为隔离对象序列化为原始 Elasticsearch 文档的方式。如今,每个原始文档 ID 都包含其空间 ID(*命名空间*)作为前缀。当对象被复制或导入到其他空间时,它们会保留相同的对象 ID,只是在序列化到 Elasticsearch 时具有不同的前缀。这导致许多 Kibana 安装在不同空间中具有相同对象 ID 的已保存对象
转换对象后,我们需要删除此前缀。由于迁移过程的限制,我们无法主动检查这是否会导致冲突。因此,我们决定预先为非默认空间中的每个对象重新生成对象 ID,以确保每个对象 ID 在全局范围内都是唯一的
如问题 2中所述,某些 URL 可能包含多个对象 ID,有效地深链接到多个对象。这些情况应由插件所有者酌情处理。一个好的经验法则是
- 页面上的“主要”对象应始终处理步骤 3中所述的三种
resolve()
结果。 -
页面上的任何“次要”对象都可以不同地处理结果。如果次要对象 ID 不重要(例如,它仅作为页面锚点),则忽略不同的结果可能更有意义。如果次要对象*很重要*但未在 UI 中直接表示,则在遇到
'conflict'
结果时抛出描述性错误可能更有意义。-
可嵌入内容应使用
spacesApi.ui.components.getEmbeddableLegacyUrlConflict
来呈现冲突错误查看详细信息向用户显示如何使用_disable_legacy_url_aliases API禁用别名并解决问题
- 如果次要对象由外部服务(例如索引模式服务)解析,则该服务应仅向使用者提供完整的结果。
-
理想情况下,如果深层链接页面上的次要对象解析为 'aliasMatch'
结果,则使用者应将用户重定向到具有新 ID 的 URL 并显示 toast 消息。这样做的原因是我们不希望用户过度依赖旧版 URL 别名。但是,这种对次要对象的处理对于 8.0 版本而言并不重要。
如上所述,当对象转换为可共享时,如果它存在于非默认空间中,则其 ID 会更改。为了保留其旧 ID,我们还创建了一个称为旧版 URL 别名(简称“别名”)的特殊对象;此别名保留目标对象的旧 ID(*sourceId*),并且包含指向目标对象的新 ID(*targetId*)的指针。
默认情况下,别名对最终用户几乎不可见。没有直接管理它们的 UI。我们的愿景是,别名将用作帮助我们完成 8.0 升级过程的权宜之计,但我们会鼓励用户不要依赖别名,以便我们最终可以弃用并删除它们。
resolve()
函数检查是否存在具有给定 ID 的对象,*以及*是否存在具有给定 ID 的别名的对象。
- 如果只有前者为真,则结果为
'exactMatch'
,表示我们找到了要查找的确切对象。 - 如果只有后者为真,则结果为
'aliasMatch'
,表示我们找到了一个具有此 ID 的别名,该别名指向具有不同 ID 的对象。 - 最后,如果*两个条件*都为真,则结果为
'conflict'
,表示我们找到了两个使用此 ID 的对象。在这种情况下,为了提高可用性,我们决定返回*最正确的匹配*,即完全匹配,而不是返回错误。通过通知使用者这是一个冲突,使用者可以向最终用户呈现适当的 UI,说明这可能不是他们实际查找的对象。
结果 1
当你使用其当前 ID 解析对象时,结果为 'exactMatch'
这可能发生在默认空间*和*非默认空间中。
结果 2
当你使用其旧 ID(其别名的 ID)解析对象时,结果为 'aliasMatch'
此结果只能发生在非默认空间中。
结果 3
第三种结果是一种边缘情况,是其他两种情况的组合。如果你解析一个对象 ID 并找到两个对象(一个作为完全匹配,另一个作为别名匹配),则结果为 'conflict'
实际上,我们已经采取了控制措施,以防止在你共享、导入或复制对象时发生这种情况。但是,如果以某种方式创建对象或用户篡改对象的原始 ES 文档,则*可能*还会在几种不同的情况下发生这种情况。由于我们无法 100% 排除这种情况,因此我们必须妥善处理,但我们预计这种情况很少发生。
重要的是要注意,当发生 'conflict'
时,返回的对象是“最正确”的匹配,即 ID 完全匹配的对象。
阅读本指南后,你可能会认为在任何地方使用 resolve()
而不是 get()
更安全或更好。实际上,我们明确决定添加一个单独的 resolve()
函数,因为我们希望限制旧版 URL 别名的影响和依赖。为此,我们根据 resolve()
的使用次数和遇到的不同结果收集匿名使用数据。如果 resolve()
的使用频率高于必要频率,则该使用数据的用处不大。
最终,resolve()
应该*仅*用于涉及用户控制的指向对象的深层链接的数据流。没有理由更改任何其他数据流以使用 resolve()
。
外部插件(未随 Kibana 提供的插件)可以使用本指南将任何隔离对象转换为可共享或完全可共享!如果你是一名外部插件开发人员,则步骤相同,但你无需担心在特定版本之前完成任何工作。你只需要知道你的插件在 8.0 版本之前无法转换你的对象。
有关用户预期将如何受到影响的更多详细信息,请参阅已保存对象 ID文档。