Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { LensAttributes } from '../../../../server/content_management';
import type { LensSOAttributesV0 } from '../../../../server/content_management/v0';

export const LENS_UNKNOWN_VIS = 'UNKNOWN';

/**
* Cleanup null and loose SO attribute types
* - `description` should not allow `null`
* - `visualizationType` should not allow `null` or `undefined`
*/
export function attributesCleanup(attributes: LensSOAttributesV0): LensAttributes {
return {
...attributes,
// fix type mismatches, null -> undefined
description: attributes.description ?? undefined,
// fix type mismatches, null | undefined -> string
visualizationType: attributes.visualizationType ?? LENS_UNKNOWN_VIS, // should never happen
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { MetricVisualizationState } from '../../../../public';
import type { LensAttributes } from '../../../../server/content_management';

/**
* Cleanup metric properties
* - Move `valuesTextAlign` to `primaryAlign` and `secondaryAlign`
* - Move `secondaryPrefix` to `secondaryLabel`
*/
export function metricMigrations(attributes: LensAttributes): LensAttributes {
if (!attributes.state || attributes.visualizationType !== 'lnsMetric') {
return attributes;
}

const state = attributes.state as {
visualization: MetricVisualizationState;
};
const newVisualizationState = getUpdatedMetricState(state.visualization);

return {
...attributes,
state: {
...state,
visualization: newVisualizationState,
},
};
}

const getUpdatedMetricState = (state: MetricVisualizationState): MetricVisualizationState => {
const { secondaryPrefix, valuesTextAlign, ...restState } = state;
let newState = { ...restState };

if (valuesTextAlign) {
newState = {
...newState,
primaryAlign: state.primaryAlign ?? valuesTextAlign,
secondaryAlign: state.secondaryAlign ?? valuesTextAlign,
};
}

if (secondaryPrefix && !newState.secondaryLabel) {
newState = {
...newState,
secondaryLabel: secondaryPrefix,
};
}

return newState;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,41 @@ import type { ColumnMeta } from './utils';

/**
* Converts old stringified colorMapping configs to new raw value configs
*
* Also fixes loop mode issue https://github.com/elastic/kibana/issues/231165
*/
export function convertToRawColorMappings(
colorMapping: DeprecatedColorMappingConfig | ColorMapping.Config,
{ ...colorMapping }: DeprecatedColorMappingConfig | ColorMapping.Config,
columnMeta?: ColumnMeta | null
): ColorMapping.Config {
const isLegacyLoopMode =
('assignmentMode' in colorMapping && colorMapping.assignmentMode === 'auto') ||
colorMapping.assignments.length === 0;
delete (colorMapping as DeprecatedColorMappingConfig).assignmentMode;

return {
...colorMapping,
assignments: colorMapping.assignments.map((oldAssignment) => {
if (isValidColorMappingAssignment(oldAssignment)) return oldAssignment;
return convertColorMappingAssignment(oldAssignment, columnMeta);
}),
specialAssignments: colorMapping.specialAssignments.map((oldAssignment) => {
if (isValidColorMappingAssignment(oldAssignment)) return oldAssignment;
specialAssignments: colorMapping.specialAssignments.map((oldAssignment, i) => {
const isBadColor = isLegacyLoopMode && i === 0;
const newColor = isBadColor
? ({
type: 'loop',
} satisfies ColorMapping.Config['specialAssignments'][number]['color'])
: oldAssignment.color;

if (isValidColorMappingAssignment(oldAssignment)) {
return {
...oldAssignment,
color: newColor,
};
}

return {
color: oldAssignment.color,
color: newColor,
touched: oldAssignment.touched,
rules: [oldAssignment.rule],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ interface DeprecatedColorMappingGradientColorMode {
* @deprecated Use `ColorMapping.Config`
*/
export interface DeprecatedColorMappingConfig {
assignmentMode?: 'auto';
paletteId: string;
colorMode: DeprecatedColorMappingCategoricalColorMode | DeprecatedColorMappingGradientColorMode;
assignments: Array<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
*/

import type { LensAttributes, LensSavedObject } from '../../../../server/content_management/v1';
import { addVersion } from './add_version';
import { convertToLegendStats } from './legend_stats';
import { convertToRawColorMappingsFn } from './raw_color_mappings';
import { convertToLegendStats } from './legend_stats';
import { attributesCleanup } from './attributes';
import { metricMigrations } from './metric';
import { addVersion } from './add_version';
import type { LensSavedObjectV0, LensAttributesV0 } from './types';

/**
Expand All @@ -17,15 +19,21 @@ import type { LensSavedObjectV0, LensAttributesV0 } from './types';
* Includes:
* - Legend value → Legend stats
* - Stringified color mapping values → Raw color mappings values
* - Fix color mapping loop mode
* - Cleanup Lens SO attributes
* - Cleanup metric properties
* - Add version property
*/
export function transformToV1LensItemAttributes(
attributes: LensAttributesV0 | LensAttributes
): LensAttributes {
return [convertToLegendStats, convertToRawColorMappingsFn, addVersion].reduce(
(newState, fn) => fn(newState),
attributes
);
return [
convertToLegendStats,
convertToRawColorMappingsFn,
attributesCleanup,
metricMigrations,
addVersion,
].reduce((newState, fn) => fn(newState), attributes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'
import { noop } from 'lodash';
import type { HttpStart } from '@kbn/core/public';
import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
import type { LensAttributes } from '../server/content_management';
import { extract, inject } from '../common/embeddable_factory';
import { LensDocumentService } from './persistence';
import { DOC_TYPE } from '../common/constants';
Expand All @@ -32,7 +31,7 @@ export interface LensAttributesService {
managed: boolean;
}>;
saveToLibrary: (
attributes: LensAttributes,
attributes: LensSavedObjectAttributes,
references: Reference[],
savedObjectId?: string
) => Promise<string>;
Expand All @@ -48,7 +47,7 @@ export interface LensAttributesService {
}

export const savedObjectToEmbeddableAttributes = (
savedObject: SavedObjectCommon<LensAttributes>
savedObject: SavedObjectCommon<LensSavedObjectAttributes>
): LensSavedObjectAttributes => {
return {
...savedObject.attributes,
Expand All @@ -73,9 +72,7 @@ export function getLensAttributeService(http: HttpStart): LensAttributesService
return {
attributes: {
...item,
visualizationType: item.visualizationType ?? null,
state: item.state as LensSavedObjectAttributes['state'],
references: item.references,
},
sharingSavedObjectProps: {
aliasTargetId: meta.aliasTargetId,
Expand All @@ -87,13 +84,12 @@ export function getLensAttributeService(http: HttpStart): LensAttributesService
};
},
saveToLibrary: async (
attributes: LensAttributes,
attributes: LensSavedObjectAttributes,
references: Reference[],
savedObjectId?: string
) => {
const result = await lensDocumentService.save({
...attributes,
visualizationType: attributes.visualizationType ?? null,
state: attributes.state as LensSavedObjectAttributes['state'],
references,
savedObjectId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { coreMock } from '@kbn/core/public/mocks';

import type { LooseLensAttributes } from './lens_client';
import { LensClient } from './lens_client';

const mockResponse = {
data: {},
meta: {},
};

const mockAttributes: LooseLensAttributes = {
title: 'Test Visualization',
visualizationType: 'lensXY',
state: {
visualization: {},
},
version: 1,
description: 'bar',
};

describe('LensClient', () => {
const httpMock = coreMock.createStart().http;
const client = new LensClient(httpMock);

beforeAll(() => {
httpMock.get.mockResolvedValue(mockResponse);
httpMock.post.mockResolvedValue(mockResponse);
httpMock.put.mockResolvedValue(mockResponse);
httpMock.delete.mockResolvedValue({ response: { ok: true } });
});

beforeEach(() => {
jest.clearAllMocks();
});

it.todo('get');
it.todo('update');
it.todo('delete');

describe('create', () => {
it('should throw an error if visualizationType is null', async () => {
await expect(
client.create(
{
...mockAttributes,
visualizationType: null,
},
[]
)
).rejects.toThrowErrorMatchingInlineSnapshot(`"Missing visualization type"`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ import {
type LensSearchRequestQuery,
type LensSearchResponseBody,
} from '../../server';
import type { LensSavedObjectAttributes } from '../react_embeddable/types';

/**
* This type is to allow `visualizationType` to be `null` in the public context.
*
* The stored attributes must have a `visualizationType`.
*/
export type LooseLensAttributes = Omit<LensAttributes, 'visualizationType'> &
Pick<LensSavedObjectAttributes, 'visualizationType'>;

export class LensClient {
constructor(private http: HttpStart) {}
Expand All @@ -37,10 +46,14 @@ export class LensClient {
}

async create(
{ description, visualizationType, state, title, version }: LensAttributes,
{ description, visualizationType, state, title, version }: LooseLensAttributes,
references: Reference[],
options: LensCreateRequestBody['options'] = {}
) {
if (visualizationType === null) {
throw new Error('Missing visualization type');
}

const body: LensCreateRequestBody = {
// TODO: Find a better way to conditionally omit id
data: omit(
Expand Down Expand Up @@ -71,10 +84,14 @@ export class LensClient {

async update(
id: string,
{ description, visualizationType, state, title, version }: LensAttributes,
{ description, visualizationType, state, title, version }: LooseLensAttributes,
references: Reference[],
options: LensUpdateRequestBody['options'] = {}
) {
if (visualizationType === null) {
throw new Error('Missing visualization type');
}

const body: LensUpdateRequestBody = {
// TODO: Find a better way to conditionally omit id
data: omit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ describe('LensStore', () => {
};

jest.mocked(client.create).mockImplementation(async (item, references) => ({
item: { id: 'new-id', ...item, references, extraProp: 'test' },
item: {
id: 'new-id',
...item,
references,
extraProp: 'test',
visualizationType: item.visualizationType ?? 'lnsXY',
},
meta: { type: 'lens' },
}));
const doc = await service.save(docToSave);
Expand Down Expand Up @@ -84,7 +90,13 @@ describe('LensStore', () => {
};

jest.mocked(client.update).mockImplementation(async (id, item, references) => ({
item: { id, ...item, references, extraProp: 'test' },
item: {
id,
...item,
references,
extraProp: 'test',
visualizationType: item.visualizationType ?? 'lnsXY',
},
meta: { type: 'lens' },
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,25 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
const requestBodyData = req.body.data;
if (!requestBodyData.visualizationType) {
throw new Error('visualizationType is required');
}

// TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(LENS_CONTENT_TYPE);

const { references, ...lensItem } = isNewApiFormat(req.body.data)
const { references, ...lensItem } = isNewApiFormat(requestBodyData)
? // TODO: Find a better way to conditionally omit id
omit(ConfigBuilderStub.in(req.body.data), 'id')
omit(ConfigBuilderStub.in(requestBodyData), 'id')
: // For now we need to be able to create old SO, this may be moved to the config builder
({
...req.body.data,
description: req.body.data.description ?? undefined,
...requestBodyData,
// fix type mismatches, null -> undefined
description: requestBodyData.description ?? undefined,
visualizationType: requestBodyData.visualizationType,
} satisfies LensCreateIn['data']);

try {
Expand Down
Loading