Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enhancements for legacy URL aliases #125960

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions docs/api/saved-objects/bulk_resolve.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,23 @@ The API returns the following:

Only the index pattern exists, the dashboard was not found.

The `outcome` field may be any of the following:

* `"exactMatch"` -- One document exactly matched the given ID, *or* {kib} failed to find this object.
* `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID.
* `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.

If the outcome is `"aliasMatch"` or `"conflict"`, the response will also include an `alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID.
[NOTE]
====
In addition to `saved_object`, several fields can be returned:

* `outcome` (required string) -- One of the following values:
- `"exactMatch"` -- One document exactly matched the given ID.
- `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than
the given ID.
- `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the
`saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.
* `alias_target_id` (optional string) -- If the `outcome` is `"aliasMatch"` or `"conflict"`, the response will also include the
`alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID.
* `suppress_redirect_toast` (optional boolean) -- If the `outcome` is `"aliasMatch"`, the response can also include an optional
`suppress_redirect_toast` boolean field. This means that the browser should not show a toast after redirecting the user.

Client-side code uses these fields to behave differently depending on the `outcome` -- <<sharing-saved-objects-step-3,learn more>>.
====

Retrieve a dashboard object in the `testspace` by ID:

Expand Down
24 changes: 17 additions & 7 deletions docs/api/saved-objects/resolve.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,23 @@ The API returns the following:
}
--------------------------------------------------

The `outcome` field may be any of the following:

* `"exactMatch"` -- One document exactly matched the given ID.
* `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID.
* `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.

If the outcome is `"aliasMatch"` or `"conflict"`, the response will also include an `alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID.
[NOTE]
====
In addition to `saved_object`, several fields can be returned:

* `outcome` (required string) -- One of the following values:
- `"exactMatch"` -- One document exactly matched the given ID.
- `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than
the given ID.
- `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the
`saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID.
* `alias_target_id` (optional string) -- If the `outcome` is `"aliasMatch"` or `"conflict"`, the response will also include the
`alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID.
* `suppress_redirect_toast` (optional boolean) -- If the `outcome` is `"aliasMatch"`, the response can also include an optional
`suppress_redirect_toast` boolean field. This means that the browser should not show a toast after redirecting the user.

Client-side code uses these fields to behave differently depending on the `outcome` -- <<sharing-saved-objects-step-3,learn more>>.
====

Retrieve a dashboard object in the `testspace` by ID:

Expand Down
16 changes: 12 additions & 4 deletions docs/developer/advanced/sharing-saved-objects.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,14 @@ TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#use

The
https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md[SavedObjectsResolveResponse
interface] has three fields, summarized below:
interface] has four fields, summarized below:

* `saved_object` - The saved object that was found.
* `outcome` - One of the following values: `'exactMatch' | 'aliasMatch' | 'conflict'`
* `alias_target_id` - This is defined if the outcome is `'aliasMatch'` or `'conflict'`. It means that a legacy URL alias with this ID points
to an object with a _different_ ID.
* `suppress_redirect_toast` - This can be defined if the outcome is `'aliasMatch'`. It means that a legacy URL alias was intentionally
created by a user, and when Kibana redirects the browser using this alias, it should not show a toast message.

The SavedObjectsClient is available both on the server-side and the client-side. You may be fetching the object on the server-side via a
custom HTTP route, or you may be fetching it on the client-side directly. Either way, the `outcome` and `alias_target_id` fields need to be
Expand Down Expand Up @@ -236,12 +238,18 @@ 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(newPath, OBJECT_NOUN);
await spacesApi.ui.redirectLegacyUrl({
path: newPath,
suppressRedirectToast: !!resolveResult.suppress_redirect_toast, <1>
objectNoun: OBJECT_NOUN <2>
});
return;
}
```
_Note that `OBJECT_NOUN` is optional, it just changes "object" in the toast to whatever you specify -- you may want the toast to say
"dashboard" or "index pattern" instead!_
<1> The `suppressRedirectToast` field is required as of 8.2, because the API response can now include an optional boolean to instruct the
client whether a toast should be shown or not.
<2> The `objectNoun` field is optional, it just changes "object" in the toast to whatever you specify -- you may want the toast to say
"dashboard" or "index pattern" instead!

5. And finally, in your deep link page, add a function that will create a callout in the case of a `'conflict'` outcome:
+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export interface ResolvedSimpleSavedObject<T = unknown>
| [alias\_target\_id?](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) | SavedObjectsResolveResponse\['alias\_target\_id'\] | <i>(Optional)</i> The ID of the object that the legacy URL alias points to. This is only defined when the outcome is <code>'aliasMatch'</code> or <code>'conflict'</code>. |
| [outcome](./kibana-plugin-core-public.resolvedsimplesavedobject.outcome.md) | SavedObjectsResolveResponse\['outcome'\] | The outcome for a successful <code>resolve</code> call is one of the following values:<!-- -->\* <code>'exactMatch'</code> -- One document exactly matched the given ID. \* <code>'aliasMatch'</code> -- One document with a legacy URL alias matched the given ID; in this case the <code>saved_object.id</code> field is different than the given ID. \* <code>'conflict'</code> -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the <code>saved_object</code> object is the exact match, and the <code>saved_object.id</code> field is the same as the given ID. |
| [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) | SimpleSavedObject&lt;T&gt; | The saved object that was found. |
| [suppress\_redirect\_toast?](./kibana-plugin-core-public.resolvedsimplesavedobject.suppress_redirect_toast.md) | boolean | <i>(Optional)</i> Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is <code>'aliasMatch'</code>. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) &gt; [suppress\_redirect\_toast](./kibana-plugin-core-public.resolvedsimplesavedobject.suppress_redirect_toast.md)

## ResolvedSimpleSavedObject.suppress\_redirect\_toast property

Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is `'aliasMatch'`<!-- -->.

<b>Signature:</b>

```typescript
suppress_redirect_toast?: boolean;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface SavedObjectsResolveResponse<T = unknown>
| [alias\_target\_id?](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) | string | <i>(Optional)</i> The ID of the object that the legacy URL alias points to. This is only defined when the outcome is <code>'aliasMatch'</code> or <code>'conflict'</code>. |
| [outcome](./kibana-plugin-core-public.savedobjectsresolveresponse.outcome.md) | 'exactMatch' \| 'aliasMatch' \| 'conflict' | The outcome for a successful <code>resolve</code> call is one of the following values:<!-- -->\* <code>'exactMatch'</code> -- One document exactly matched the given ID. \* <code>'aliasMatch'</code> -- One document with a legacy URL alias matched the given ID; in this case the <code>saved_object.id</code> field is different than the given ID. \* <code>'conflict'</code> -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the <code>saved_object</code> object is the exact match, and the <code>saved_object.id</code> field is the same as the given ID. |
| [saved\_object](./kibana-plugin-core-public.savedobjectsresolveresponse.saved_object.md) | SavedObject&lt;T&gt; | The saved object that was found. |
| [suppress\_redirect\_toast?](./kibana-plugin-core-public.savedobjectsresolveresponse.suppress_redirect_toast.md) | boolean | <i>(Optional)</i> Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is <code>'aliasMatch'</code>. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [SavedObjectsResolveResponse](./kibana-plugin-core-public.savedobjectsresolveresponse.md) &gt; [suppress\_redirect\_toast](./kibana-plugin-core-public.savedobjectsresolveresponse.suppress_redirect_toast.md)

## SavedObjectsResolveResponse.suppress\_redirect\_toast property

Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is `'aliasMatch'`<!-- -->.

<b>Signature:</b>

```typescript
suppress_redirect_toast?: boolean;
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface SavedObjectsResolveResponse<T = unknown>
| [alias\_target\_id?](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) | string | <i>(Optional)</i> The ID of the object that the legacy URL alias points to. This is only defined when the outcome is <code>'aliasMatch'</code> or <code>'conflict'</code>. |
| [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' \| 'aliasMatch' \| 'conflict' | The outcome for a successful <code>resolve</code> call is one of the following values:<!-- -->\* <code>'exactMatch'</code> -- One document exactly matched the given ID. \* <code>'aliasMatch'</code> -- One document with a legacy URL alias matched the given ID; in this case the <code>saved_object.id</code> field is different than the given ID. \* <code>'conflict'</code> -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the <code>saved_object</code> object is the exact match, and the <code>saved_object.id</code> field is the same as the given ID. |
| [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject&lt;T&gt; | The saved object that was found. |
| [suppress\_redirect\_toast?](./kibana-plugin-core-server.savedobjectsresolveresponse.suppress_redirect_toast.md) | boolean | <i>(Optional)</i> Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is <code>'aliasMatch'</code>. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) &gt; [suppress\_redirect\_toast](./kibana-plugin-core-server.savedobjectsresolveresponse.suppress_redirect_toast.md)

## SavedObjectsResolveResponse.suppress\_redirect\_toast property

Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is `'aliasMatch'`<!-- -->.

<b>Signature:</b>

```typescript
suppress_redirect_toast?: boolean;
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ Given a saved object type and id, generates the compound id that is stored in th
<b>Signature:</b>

```typescript
generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string;
generateRawLegacyUrlAliasId(namespace: string | undefined, type: string, id: string): string;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| namespace | string | The namespace of the saved object |
| namespace | string \| undefined | The namespace of the saved object |
| type | string | The saved object type |
| id | string | The id of the saved object |

Expand Down
2 changes: 2 additions & 0 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,7 @@ export interface ResolvedSimpleSavedObject<T = unknown> {
alias_target_id?: SavedObjectsResolveResponse['alias_target_id'];
outcome: SavedObjectsResolveResponse['outcome'];
saved_object: SimpleSavedObject<T>;
suppress_redirect_toast?: boolean;
}

// @public (undocumented)
Expand Down Expand Up @@ -1362,6 +1363,7 @@ export interface SavedObjectsResolveResponse<T = unknown> {
alias_target_id?: string;
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
saved_object: SavedObject<T>;
suppress_redirect_toast?: boolean;
}

// @public (undocumented)
Expand Down
1 change: 1 addition & 0 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ export class SavedObjectsClient {
saved_object: simpleSavedObject,
outcome: resolveResponse.outcome,
alias_target_id: resolveResponse.alias_target_id,
suppress_redirect_toast: resolveResponse.suppress_redirect_toast,
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/core/public/saved_objects/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ export interface ResolvedSimpleSavedObject<T = unknown> {
* The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`.
*/
alias_target_id?: SavedObjectsResolveResponse['alias_target_id'];
/**
* Whether the client should suppress the toast message upon redirecting to the new URL. This is only defined when the outcome is
* `'aliasMatch'`.
*/
suppress_redirect_toast?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -549,19 +549,17 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) {

const { id: originId, type } = otherAttrs;
const id = SavedObjectsUtils.getConvertedObjectId(namespace, type, originId!);
if (namespace !== undefined) {
const legacyUrlAlias: SavedObjectUnsanitizedDoc<LegacyUrlAlias> = {
id: `${namespace}:${type}:${originId}`,
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: originId,
targetNamespace: namespace,
targetType: type,
targetId: id,
},
};
additionalDocs.push(legacyUrlAlias);
}
const legacyUrlAlias: SavedObjectUnsanitizedDoc<LegacyUrlAlias> = {
id: `${namespace}:${type}:${originId}`,
type: LEGACY_URL_ALIAS_TYPE,
attributes: {
sourceId: originId,
targetNamespace: namespace,
targetType: type,
targetId: id,
},
};
additionalDocs.push(legacyUrlAlias);
return {
transformedDoc: { ...otherAttrs, id, originId, namespaces: [namespace] },
additionalDocs,
Expand Down
4 changes: 4 additions & 0 deletions src/core/server/saved_objects/object_types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ export interface LegacyUrlAlias {
* `SavedObjectsClient.collectMultiNamespaceReferences()`.
*/
disabled?: boolean;
/**
* Whether or not a toast should be shown when a user is redirected from a legacy URL.
*/
suppressRedirectToast?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ export const registerRoutesMock = jest.fn();
jest.doMock('./routes', () => ({
registerRoutes: registerRoutesMock,
}));

// The SavedObjectsSerializer imports SavedObjectUtils from the '../service' module, and that somehow breaks unit tests for the
// SavedObjectsService. To avoid this, we mock the entire './serialization' module, since we don't need it for these tests.
jest.mock('./serialization');
6 changes: 4 additions & 2 deletions src/core/server/saved_objects/serialization/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import typeDetect from 'type-detect';
import { LEGACY_URL_ALIAS_TYPE } from '../object_types';
import { decodeVersion, encodeVersion } from '../version';
import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry';
import { SavedObjectsUtils } from '../service';
import {
SavedObjectsRawDoc,
SavedObjectSanitizedDoc,
Expand Down Expand Up @@ -170,8 +171,9 @@ export class SavedObjectsSerializer {
* @param {string} type - The saved object type
* @param {string} id - The id of the saved object
*/
public generateRawLegacyUrlAliasId(namespace: string, type: string, id: string) {
return `${LEGACY_URL_ALIAS_TYPE}:${namespace}:${type}:${id}`;
public generateRawLegacyUrlAliasId(namespace: string | undefined, type: string, id: string) {
const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace);
return `${LEGACY_URL_ALIAS_TYPE}:${namespaceString}:${type}:${id}`;
}

/**
Expand Down
Loading