mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
savedObjects: add score
to repository.find results (#68894)
* add `score` to repository.find results * update generated doc * fix FTR result set * remove score from exports * fix FTR for find API * fix label * fix tsdoc
This commit is contained in:
parent
7dd4fa2618
commit
d2006ea8a0
26 changed files with 123 additions and 23 deletions
|
@ -157,6 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry |
|
||||
| [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | |
|
||||
| [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects <code>find()</code> method.<!-- -->\*Note\*: this type is different between the Public and Server Saved Objects clients. |
|
||||
| [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | |
|
||||
| [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. |
|
||||
| [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. |
|
||||
| [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. |
|
||||
|
|
|
@ -20,6 +20,6 @@ export interface SavedObjectsFindResponse<T = unknown>
|
|||
| --- | --- | --- |
|
||||
| [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | <code>number</code> | |
|
||||
| [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | <code>number</code> | |
|
||||
| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | <code>Array<SavedObject<T>></code> | |
|
||||
| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | <code>Array<SavedObjectsFindResult<T>></code> | |
|
||||
| [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | <code>number</code> | |
|
||||
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
saved_objects: Array<SavedObject<T>>;
|
||||
saved_objects: Array<SavedObjectsFindResult<T>>;
|
||||
```
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md)
|
||||
|
||||
## SavedObjectsFindResult interface
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | <code>number</code> | The Elasticsearch <code>_score</code> of this result. |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md)
|
||||
|
||||
## SavedObjectsFindResult.score property
|
||||
|
||||
The Elasticsearch `_score` of this result.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
score: number;
|
||||
```
|
|
@ -303,7 +303,6 @@ export class SavedObjectsClient {
|
|||
query,
|
||||
});
|
||||
return request.then((resp) => {
|
||||
resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d));
|
||||
return renameKeys<
|
||||
PromiseType<ReturnType<SavedObjectsApi['find']>>,
|
||||
SavedObjectsFindResponsePublic
|
||||
|
@ -314,7 +313,10 @@ export class SavedObjectsClient {
|
|||
per_page: 'perPage',
|
||||
page: 'page',
|
||||
},
|
||||
resp
|
||||
{
|
||||
...resp,
|
||||
saved_objects: resp.saved_objects.map((d) => this.createSavedObject(d)),
|
||||
}
|
||||
) as SavedObjectsFindResponsePublic<T>;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -217,6 +217,7 @@ export {
|
|||
SavedObjectsErrorHelpers,
|
||||
SavedObjectsExportOptions,
|
||||
SavedObjectsExportResultDetails,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsImportConflictError,
|
||||
SavedObjectsImportError,
|
||||
|
|
|
@ -47,6 +47,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
|
@ -59,6 +60,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -133,6 +135,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
|
@ -145,6 +148,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -192,6 +196,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
|
@ -204,6 +209,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -279,6 +285,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'name',
|
||||
|
@ -291,6 +298,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -366,6 +374,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '2',
|
||||
type: 'search',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
|
@ -378,6 +387,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -405,6 +415,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
attributes: {
|
||||
name: 'baz',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -413,6 +424,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
attributes: {
|
||||
name: 'foo',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -421,6 +433,7 @@ describe('getSortedObjectsForExport()', () => {
|
|||
attributes: {
|
||||
name: 'bar',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -116,8 +116,11 @@ async function fetchObjectsToExport({
|
|||
}
|
||||
|
||||
// sorts server-side by _id, since it's only available in fielddata
|
||||
return findResponse.saved_objects.sort((a: SavedObject, b: SavedObject) =>
|
||||
a.id > b.id ? 1 : -1
|
||||
return (
|
||||
findResponse.saved_objects
|
||||
// exclude the find-specific `score` property from the exported objects
|
||||
.map(({ score, ...obj }) => obj)
|
||||
.sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1))
|
||||
);
|
||||
} else {
|
||||
throw Boom.badRequest('Either `type` or `objects` are required.');
|
||||
|
|
|
@ -79,6 +79,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -88,6 +89,7 @@ describe('GET /api/saved_objects/_find', () => {
|
|||
timeFieldName: '@timestamp',
|
||||
notExpandable: true,
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1939,7 +1939,7 @@ describe('SavedObjectsRepository', () => {
|
|||
{
|
||||
_index: '.kibana',
|
||||
_id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`,
|
||||
_score: 1,
|
||||
_score: 2,
|
||||
...mockVersionProps,
|
||||
_source: {
|
||||
namespace,
|
||||
|
@ -1954,7 +1954,7 @@ describe('SavedObjectsRepository', () => {
|
|||
{
|
||||
_index: '.kibana',
|
||||
_id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`,
|
||||
_score: 1,
|
||||
_score: 3,
|
||||
...mockVersionProps,
|
||||
_source: {
|
||||
namespace,
|
||||
|
@ -1970,7 +1970,7 @@ describe('SavedObjectsRepository', () => {
|
|||
{
|
||||
_index: '.kibana',
|
||||
_id: `${NAMESPACE_AGNOSTIC_TYPE}:something`,
|
||||
_score: 1,
|
||||
_score: 4,
|
||||
...mockVersionProps,
|
||||
_source: {
|
||||
type: NAMESPACE_AGNOSTIC_TYPE,
|
||||
|
@ -2131,6 +2131,7 @@ describe('SavedObjectsRepository', () => {
|
|||
type: doc._source.type,
|
||||
...mockTimestampFields,
|
||||
version: mockVersion,
|
||||
score: doc._score,
|
||||
attributes: doc._source[doc._source.type],
|
||||
references: [],
|
||||
});
|
||||
|
@ -2153,6 +2154,7 @@ describe('SavedObjectsRepository', () => {
|
|||
type: doc._source.type,
|
||||
...mockTimestampFields,
|
||||
version: mockVersion,
|
||||
score: doc._score,
|
||||
attributes: doc._source[doc._source.type],
|
||||
references: [],
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
|
@ -674,8 +675,11 @@ export class SavedObjectsRepository {
|
|||
page,
|
||||
per_page: perPage,
|
||||
total: response.hits.total,
|
||||
saved_objects: response.hits.hits.map((hit: SavedObjectsRawDoc) =>
|
||||
this._rawToSavedObject(hit)
|
||||
saved_objects: response.hits.hits.map(
|
||||
(hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({
|
||||
...this._rawToSavedObject(hit),
|
||||
score: (hit as any)._score,
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -79,6 +79,17 @@ export interface SavedObjectsBulkResponse<T = unknown> {
|
|||
saved_objects: Array<SavedObject<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T> {
|
||||
/**
|
||||
* The Elasticsearch `_score` of this result.
|
||||
*/
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type of the Saved Objects `find()` method.
|
||||
*
|
||||
|
@ -88,7 +99,7 @@ export interface SavedObjectsBulkResponse<T = unknown> {
|
|||
* @public
|
||||
*/
|
||||
export interface SavedObjectsFindResponse<T = unknown> {
|
||||
saved_objects: Array<SavedObject<T>>;
|
||||
saved_objects: Array<SavedObjectsFindResult<T>>;
|
||||
total: number;
|
||||
per_page: number;
|
||||
page: number;
|
||||
|
|
|
@ -2039,11 +2039,16 @@ export interface SavedObjectsFindResponse<T = unknown> {
|
|||
// (undocumented)
|
||||
per_page: number;
|
||||
// (undocumented)
|
||||
saved_objects: Array<SavedObject<T>>;
|
||||
saved_objects: Array<SavedObjectsFindResult<T>>;
|
||||
// (undocumented)
|
||||
total: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SavedObjectsFindResult<T = unknown> extends SavedObject<T> {
|
||||
score: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface SavedObjectsImportConflictError {
|
||||
// (undocumented)
|
||||
|
|
|
@ -18,17 +18,18 @@
|
|||
*/
|
||||
|
||||
import { times } from 'lodash';
|
||||
import { SavedObjectsFindOptions, SavedObject } from 'src/core/server';
|
||||
import { SavedObjectsFindOptions, SavedObjectsFindResult } from 'src/core/server';
|
||||
import { savedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
import { findAll } from './find_all';
|
||||
|
||||
describe('findAll', () => {
|
||||
let savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
|
||||
|
||||
const createObj = (id: number): SavedObject => ({
|
||||
const createObj = (id: number): SavedObjectsFindResult => ({
|
||||
type: 'type',
|
||||
id: `id-${id}`,
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ describe('findRelationships', () => {
|
|||
type: 'parent-type',
|
||||
id: 'parent-id',
|
||||
attributes: {},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -46,6 +46,7 @@ export default function ({ getService }) {
|
|||
attributes: {
|
||||
title: 'Count of requests',
|
||||
},
|
||||
score: 0,
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
|
@ -134,6 +135,7 @@ export default function ({ getService }) {
|
|||
.searchSourceJSON,
|
||||
},
|
||||
},
|
||||
score: 0,
|
||||
references: [
|
||||
{
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
|
|
|
@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
score: 0,
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
meta: {
|
||||
editUrl:
|
||||
|
|
|
@ -386,6 +386,7 @@ describe('getAll()', () => {
|
|||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1667,6 +1667,7 @@ describe('find()', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
|
|
|
@ -222,7 +222,12 @@ describe('Utils', () => {
|
|||
];
|
||||
|
||||
const res = transformCases(
|
||||
{ saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 },
|
||||
{
|
||||
saved_objects: mockCases.map((obj) => ({ ...obj, score: 1 })),
|
||||
total: mockCases.length,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
},
|
||||
2,
|
||||
2,
|
||||
extraCaseData,
|
||||
|
@ -232,7 +237,11 @@ describe('Utils', () => {
|
|||
page: 1,
|
||||
per_page: 10,
|
||||
total: mockCases.length,
|
||||
cases: flattenCaseSavedObjects(mockCases, extraCaseData, '123'),
|
||||
cases: flattenCaseSavedObjects(
|
||||
mockCases.map((obj) => ({ ...obj, score: 1 })),
|
||||
extraCaseData,
|
||||
'123'
|
||||
),
|
||||
count_open_cases: 2,
|
||||
count_closed_cases: 2,
|
||||
});
|
||||
|
@ -500,7 +509,7 @@ describe('Utils', () => {
|
|||
describe('transformComments', () => {
|
||||
it('transforms correctly', () => {
|
||||
const comments = {
|
||||
saved_objects: mockCaseComments,
|
||||
saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })),
|
||||
total: mockCaseComments.length,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
|
|
|
@ -101,7 +101,7 @@ export const transformCases = (
|
|||
});
|
||||
|
||||
export const flattenCaseSavedObjects = (
|
||||
savedObjects: SavedObjectsFindResponse<CaseAttributes>['saved_objects'],
|
||||
savedObjects: Array<SavedObject<CaseAttributes>>,
|
||||
totalCommentByCase: TotalCommentByCase[],
|
||||
caseConfigureConnectorId: string = 'none'
|
||||
): CaseResponse[] =>
|
||||
|
@ -146,7 +146,7 @@ export const transformComments = (
|
|||
});
|
||||
|
||||
export const flattenCommentSavedObjects = (
|
||||
savedObjects: SavedObjectsFindResponse<CommentAttributes>['saved_objects']
|
||||
savedObjects: Array<SavedObject<CommentAttributes>>
|
||||
): CommentResponse[] =>
|
||||
savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject<CommentAttributes>) => {
|
||||
return [...acc, flattenCommentSavedObject(savedObject)];
|
||||
|
|
|
@ -676,12 +676,14 @@ describe('#find', () => {
|
|||
id: 'some-id',
|
||||
type: 'unknown-type',
|
||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: 'some-id-2',
|
||||
type: 'unknown-type',
|
||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -722,6 +724,7 @@ describe('#find', () => {
|
|||
attrNotSoSecret: 'not-so-secret',
|
||||
attrThree: 'three',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -733,6 +736,7 @@ describe('#find', () => {
|
|||
attrNotSoSecret: '*not-so-secret*',
|
||||
attrThree: 'three',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
@ -793,6 +797,7 @@ describe('#find', () => {
|
|||
attrNotSoSecret: 'not-so-secret',
|
||||
attrThree: 'three',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
|
@ -804,6 +809,7 @@ describe('#find', () => {
|
|||
attrNotSoSecret: '*not-so-secret*',
|
||||
attrThree: 'three',
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -549,6 +549,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<
|
|||
searchAfterTimeDurations: ['200.00'],
|
||||
bulkCreateTimeDurations: ['800.43'],
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
updated_at: '2020-02-18T15:26:51.333Z',
|
||||
version: 'WzQ2LDFd',
|
||||
|
@ -570,6 +571,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<
|
|||
searchAfterTimeDurations: ['200.00'],
|
||||
bulkCreateTimeDurations: ['800.43'],
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
updated_at: '2020-02-18T15:15:58.860Z',
|
||||
version: 'WzMyLDFd',
|
||||
|
|
|
@ -391,7 +391,7 @@ export const exampleFindRuleStatusResponse: (
|
|||
total: 1,
|
||||
per_page: 6,
|
||||
page: 1,
|
||||
saved_objects: mockStatuses,
|
||||
saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })),
|
||||
});
|
||||
|
||||
export const mockLogger: Logger = loggingServiceMock.createLogger();
|
||||
|
|
|
@ -138,7 +138,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
test(`passes options.type to baseClient if valid singular type specified`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
|
||||
total: 1,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
|
@ -158,7 +158,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces';
|
|||
test(`supplements options with the current namespace`, async () => {
|
||||
const { client, baseClient } = await createSpacesSavedObjectsClient();
|
||||
const expectedReturnValue = {
|
||||
saved_objects: [createMockResponse()],
|
||||
saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })),
|
||||
total: 1,
|
||||
per_page: 0,
|
||||
page: 0,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue