fix(Dashboard): Sync color configuration via dedicated endpoint (#31374)

This commit is contained in:
Geido 2024-12-13 15:58:02 +02:00 committed by GitHub
parent bf56a327f4
commit e1f98e246f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 445 additions and 128 deletions

View File

@ -73,8 +73,9 @@ import {
isLabelsColorMapSynced,
getColorSchemeDomain,
getColorNamespace,
getLabelsColorMapEntries,
getFreshLabelsColorMapEntries,
getFreshSharedLabels,
getDynamicLabelsColors,
} from '../../utils/colorScheme';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
@ -253,15 +254,18 @@ export function setDashboardSharedLabelsColorsSynced() {
return { type: SET_DASHBOARD_SHARED_LABELS_COLORS_SYNCED };
}
export const setDashboardMetadata = updatedMetadata => async dispatch => {
export const setDashboardMetadata =
updatedMetadata => async (dispatch, getState) => {
const { dashboardInfo } = getState();
dispatch(
dashboardInfoChanged({
metadata: {
...(dashboardInfo?.metadata || {}),
...updatedMetadata,
},
}),
);
};
};
export function saveDashboardRequest(data, id, saveType) {
return (dispatch, getState) => {
@ -320,7 +324,7 @@ export function saveDashboardRequest(data, id, saveType) {
expanded_slices: data.metadata?.expanded_slices || {},
label_colors: customLabelsColor,
shared_label_colors: getFreshSharedLabels(sharedLabelsColor),
map_label_colors: getLabelsColorMapEntries(customLabelsColor),
map_label_colors: getFreshLabelsColorMapEntries(customLabelsColor),
refresh_frequency: data.metadata?.refresh_frequency || 0,
timed_refresh_immune_slices:
data.metadata?.timed_refresh_immune_slices || [],
@ -719,11 +723,18 @@ export function setDatasetsStatus(status) {
};
}
const storeDashboardMetadata = async (id, metadata) =>
const storeDashboardColorConfig = async (id, metadata) =>
SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
endpoint: `/api/v1/dashboard/${id}/colors?mark_updated=false`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json_metadata: JSON.stringify(metadata) }),
body: JSON.stringify({
color_namespace: metadata.color_namespace,
color_scheme: metadata.color_scheme,
color_scheme_domain: metadata.color_scheme_domain || [],
shared_label_colors: metadata.shared_label_colors || [],
map_label_colors: metadata.map_label_colors || {},
label_colors: metadata.label_colors || {},
}),
});
/**
@ -742,7 +753,7 @@ export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
if (labelsColorMapMustSync || sharedLabelsColorsMustSync) {
dispatch(setDashboardLabelsColorMapSynced());
dispatch(setDashboardSharedLabelsColorsSynced());
storeDashboardMetadata(id, metadata);
storeDashboardColorConfig(id, metadata);
}
};
@ -756,7 +767,6 @@ export const persistDashboardLabelsColor = () => async (dispatch, getState) => {
*/
export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
try {
const updatedMetadata = { ...metadata };
const customLabelsColor = metadata.label_colors || {};
let hasChanged = false;
@ -764,11 +774,14 @@ export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
const sharedLabels = metadata.shared_label_colors || [];
if (!Array.isArray(sharedLabels) && Object.keys(sharedLabels).length > 0) {
hasChanged = true;
updatedMetadata.shared_label_colors = [];
dispatch(
setDashboardMetadata({
shared_label_colors: [],
}),
);
}
// backward compatibility of map_label_colors
const hasMapLabelColors =
Object.keys(metadata.map_label_colors || {}).length > 0;
const hasMapLabelColors = !!metadata.map_label_colors;
let updatedScheme = metadata.color_scheme;
const categoricalSchemes = getCategoricalSchemeRegistry();
@ -780,11 +793,14 @@ export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
const defaultScheme = categoricalSchemes.defaultKey;
const fallbackScheme = defaultScheme?.toString() || 'supersetColors';
hasChanged = true;
updatedScheme = fallbackScheme;
updatedMetadata.color_scheme = updatedScheme;
dispatch(setColorScheme(updatedScheme));
dispatch(
setDashboardMetadata({
color_scheme: updatedScheme,
}),
);
}
// the stored color domain registry and fresh might differ at this point
@ -795,24 +811,28 @@ export const applyDashboardLabelsColorOnLoad = metadata => async dispatch => {
if (!isEqual(freshColorSchemeDomain, currentColorSchemeDomain)) {
hasChanged = true;
updatedMetadata.color_scheme_domain = freshColorSchemeDomain;
dispatch(
setDashboardMetadata({
color_scheme_domain: freshColorSchemeDomain,
}),
);
}
// if color scheme is invalid or map is missing, apply a fresh color map
// if valid, apply the stored map to keep consistency across refreshes
const shouldGoFresh = !hasMapLabelColors || hasInvalidColorScheme;
applyColors(updatedMetadata, shouldGoFresh);
applyColors(metadata, shouldGoFresh);
if (shouldGoFresh) {
// a fresh color map has been applied
// needs to be stored for consistency
hasChanged = true;
updatedMetadata.map_label_colors =
getLabelsColorMapEntries(customLabelsColor);
dispatch(
setDashboardMetadata({
map_label_colors: getFreshLabelsColorMapEntries(customLabelsColor),
}),
);
}
if (hasChanged) {
dispatch(setDashboardMetadata(updatedMetadata));
dispatch(setDashboardLabelsColorMapSync());
}
} catch (e) {
@ -832,19 +852,28 @@ export const ensureSyncedLabelsColorMap = metadata => (dispatch, getState) => {
const {
dashboardState: { labelsColorMapMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const customLabelsColor = metadata.label_colors || {};
const isMapSynced = isLabelsColorMapSynced(metadata);
const mustSync = !isMapSynced;
const fullLabelsColors = getDynamicLabelsColors(
metadata.map_label_colors || {},
customLabelsColor,
);
const freshColorMapEntries =
getFreshLabelsColorMapEntries(customLabelsColor);
const isMapSynced = isLabelsColorMapSynced(
fullLabelsColors,
freshColorMapEntries,
customLabelsColor,
);
if (mustSync) {
const freshestColorMapEntries =
getLabelsColorMapEntries(customLabelsColor);
updatedMetadata.map_label_colors = freshestColorMapEntries;
dispatch(setDashboardMetadata(updatedMetadata));
if (!isMapSynced) {
dispatch(
setDashboardMetadata({
map_label_colors: freshColorMapEntries,
}),
);
}
if (mustSync && !labelsColorMapMustSync) {
if (!isMapSynced && !labelsColorMapMustSync) {
// prepare to persist the just applied labels color map
dispatch(setDashboardLabelsColorMapSync());
}
@ -867,7 +896,6 @@ export const ensureSyncedSharedLabelsColors =
const {
dashboardState: { sharedLabelsColorsMustSync },
} = getState();
const updatedMetadata = { ...metadata };
const sharedLabelsColors = enforceSharedLabelsColorsArray(
metadata.shared_label_colors,
);
@ -875,15 +903,17 @@ export const ensureSyncedSharedLabelsColors =
forceFresh ? [] : sharedLabelsColors,
);
const isSharedLabelsColorsSynced = isEqual(
sharedLabelsColors,
freshLabelsColors,
sharedLabelsColors.sort(),
freshLabelsColors.sort(),
);
const mustSync = !isSharedLabelsColorsSynced;
if (mustSync) {
updatedMetadata.shared_label_colors = freshLabelsColors;
dispatch(setDashboardMetadata(updatedMetadata));
dispatch(
setDashboardMetadata({
shared_label_colors: freshLabelsColors,
}),
);
}
if (mustSync && !sharedLabelsColorsMustSync) {
@ -901,8 +931,7 @@ export const ensureSyncedSharedLabelsColors =
* @param {*} renderedChartIds - the charts that have finished rendering
* @returns void
*/
export const updateDashboardLabelsColor =
renderedChartIds => (dispatch, getState) => {
export const updateDashboardLabelsColor = renderedChartIds => (_, getState) => {
try {
const {
dashboardInfo: { metadata },
@ -910,11 +939,14 @@ export const updateDashboardLabelsColor =
} = getState();
const colorScheme = metadata.color_scheme;
const labelsColorMapInstance = getLabelsColorMap();
const fullLabelsColors = metadata.map_label_colors || {};
const sharedLabelsColors = enforceSharedLabelsColorsArray(
metadata.shared_label_colors,
);
const customLabelsColors = metadata.label_colors || {};
const fullLabelsColors = getDynamicLabelsColors(
metadata.map_label_colors || {},
customLabelsColors,
);
// for dashboards with no color scheme, the charts should always use their individual schemes
// this logic looks for unique labels (not shared across multiple charts) of each rendered chart
@ -942,8 +974,7 @@ export const updateDashboardLabelsColor =
);
const currentChartLabels = currentChartConfig?.labels || [];
const uniqueChartLabels = currentChartLabels.filter(
l =>
!sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
l => !sharedLabelsSet.has(l) && !customLabelsColors.hasOwnProperty(l),
);
// Map unique labels to colors
@ -974,4 +1005,4 @@ export const updateDashboardLabelsColor =
} catch (e) {
console.error('Failed to update colors for new charts and labels:', e);
}
};
};

View File

@ -47,7 +47,7 @@ import { loadTags } from 'src/components/Tags/utils';
import {
applyColors,
getColorNamespace,
getLabelsColorMapEntries,
getFreshLabelsColorMapEntries,
} from 'src/utils/colorScheme';
import getOwnerName from 'src/utils/getOwnerName';
import Owner from 'src/types/Owner';
@ -369,7 +369,7 @@ const PropertiesModal = ({
dispatch(
setDashboardMetadata({
...updatedDashboardMetadata,
map_label_colors: getLabelsColorMapEntries(customLabelColors),
map_label_colors: getFreshLabelsColorMapEntries(customLabelColors),
}),
);

View File

@ -23,6 +23,8 @@ import {
getCategoricalSchemeRegistry,
getLabelsColorMap,
} from '@superset-ui/core';
import { intersection, omit, pick } from 'lodash';
import { areObjectsEqual } from 'src/reduxUtils';
const EMPTY_ARRAY: string[] = [];
@ -91,14 +93,13 @@ export const getSharedLabelsColorMapEntries = (
* @param customLabelsColor - the custom label colors in label_colors field
* @returns all color entries except custom label colors
*/
export const getLabelsColorMapEntries = (
export const getFreshLabelsColorMapEntries = (
customLabelsColor: Record<string, string> = {},
): Record<string, string> => {
const labelsColorMapInstance = getLabelsColorMap();
const allEntries = Object.fromEntries(labelsColorMapInstance.getColorMap());
// custom label colors are applied and stored separetely via label_colors
// removing all instances of custom label colors from the entries
Object.keys(customLabelsColor).forEach(label => {
delete allEntries[label];
});
@ -106,6 +107,19 @@ export const getLabelsColorMapEntries = (
return allEntries;
};
/**
* Returns all dynamic labels and colors (excluding custom label colors).
*
* @param labelsColorMap - the labels color map
* @param customLabelsColor - the custom label colors in label_colors field
* @returns all color entries except custom label colors
*/
export const getDynamicLabelsColors = (
fullLabelsColors: Record<string, string>,
customLabelsColor: Record<string, string> = {},
): Record<string, string> =>
omit(fullLabelsColors, Object.keys(customLabelsColor));
export const getColorSchemeDomain = (colorScheme: string) =>
getCategoricalSchemeRegistry().get(colorScheme)?.colors || [];
@ -116,20 +130,29 @@ export const getColorSchemeDomain = (colorScheme: string) =>
* @returns true if the labels color map is the same as fresh
*/
export const isLabelsColorMapSynced = (
metadata: Record<string, any>,
storedLabelsColors: Record<string, any>,
freshLabelsColors: Record<string, any>,
customLabelColors: Record<string, string>,
): boolean => {
const storedLabelsColorMap = metadata.map_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const freshColorMap = getLabelsColorMap().getColorMap();
const fullFreshColorMap = {
...Object.fromEntries(freshColorMap),
...customLabelColors,
};
const freshLabelsCount = Object.keys(freshLabelsColors).length;
const isSynced = Object.entries(fullFreshColorMap).every(
([label, color]) =>
storedLabelsColorMap.hasOwnProperty(label) &&
storedLabelsColorMap[label] === color,
// still updating, pass
if (!freshLabelsCount) return true;
const commonKeys = intersection(
Object.keys(storedLabelsColors),
Object.keys(freshLabelsColors),
);
const comparableStoredLabelsColors = pick(storedLabelsColors, commonKeys);
const comparableFreshLabelsColors = pick(freshLabelsColors, commonKeys);
const isSynced = areObjectsEqual(
comparableStoredLabelsColors,
comparableFreshLabelsColors,
{
ignoreFields: Object.keys(customLabelColors),
},
);
return isSynced;
@ -227,7 +250,7 @@ export const applyColors = (
if (fresh) {
// requires a new map all together
applicableColorMapEntries = {
...getLabelsColorMapEntries(customLabelsColor),
...getFreshLabelsColorMapEntries(customLabelsColor),
};
}
if (merge) {
@ -235,7 +258,7 @@ export const applyColors = (
// without overriding existing ones
applicableColorMapEntries = {
...fullLabelsColor,
...getLabelsColorMapEntries(customLabelsColor),
...getFreshLabelsColorMapEntries(customLabelsColor),
};
}

View File

@ -62,6 +62,10 @@ class DashboardNativeFiltersUpdateFailedError(UpdateFailedError):
message = _("Dashboard native filters could not be patched.")
class DashboardColorsConfigUpdateFailedError(UpdateFailedError):
message = _("Dashboard color configuration could not be updated.")
class DashboardDeleteFailedError(DeleteFailedError):
message = _("Dashboard could not be deleted.")

View File

@ -22,9 +22,10 @@ from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from superset import app, security_manager
from superset import app, db, security_manager
from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.dashboard.exceptions import (
DashboardColorsConfigUpdateFailedError,
DashboardForbiddenError,
DashboardInvalidError,
DashboardNativeFiltersUpdateFailedError,
@ -202,3 +203,29 @@ class UpdateDashboardNativeFiltersCommand(UpdateDashboardCommand):
)
return configuration
class UpdateDashboardColorsConfigCommand(UpdateDashboardCommand):
def __init__(
self, model_id: int, data: dict[str, Any], mark_updated: bool = True
) -> None:
super().__init__(model_id, data)
self._mark_updated = mark_updated
@transaction(
on_error=partial(on_error, reraise=DashboardColorsConfigUpdateFailedError)
)
def run(self) -> Model:
super().validate()
assert self._model
original_changed_on = self._model.changed_on
DashboardDAO.update_colors_config(self._model, self._properties)
if not self._mark_updated:
db.session.commit() # pylint: disable=consider-using-transaction
# restore the original changed_on value
self._model.changed_on = original_changed_on
return self._model

View File

@ -174,6 +174,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
"csv_metadata": "csv_upload",
"slack_channels": "write",
"put_filters": "write",
"put_colors": "write",
}
EXTRA_FORM_DATA_APPEND_KEYS = {

View File

@ -391,6 +391,24 @@ class DashboardDAO(BaseDAO[Dashboard]):
return updated_configuration
@classmethod
def update_colors_config(
cls, dashboard: Dashboard, attributes: dict[str, Any]
) -> None:
metadata = json.loads(dashboard.json_metadata or "{}")
for key in [
"color_scheme_domain",
"color_scheme",
"shared_label_colors",
"map_label_colors",
"label_colors",
]:
if key in attributes:
metadata[key] = attributes[key]
dashboard.json_metadata = json.dumps(metadata)
@staticmethod
def add_favorite(dashboard: Dashboard) -> None:
ids = DashboardDAO.favorited_ids([dashboard])

View File

@ -42,6 +42,7 @@ from superset.commands.dashboard.delete import (
)
from superset.commands.dashboard.exceptions import (
DashboardAccessDeniedError,
DashboardColorsConfigUpdateFailedError,
DashboardCopyError,
DashboardCreateFailedError,
DashboardDeleteFailedError,
@ -57,6 +58,7 @@ from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCom
from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
from superset.commands.dashboard.unfave import DelFavoriteDashboardCommand
from superset.commands.dashboard.update import (
UpdateDashboardColorsConfigCommand,
UpdateDashboardCommand,
UpdateDashboardNativeFiltersCommand,
)
@ -81,6 +83,7 @@ from superset.dashboards.permalink.types import DashboardPermalinkState
from superset.dashboards.schemas import (
CacheScreenshotSchema,
DashboardCacheScreenshotResponseSchema,
DashboardColorsConfigUpdateSchema,
DashboardCopySchema,
DashboardDatasetSchema,
DashboardGetResponseSchema,
@ -108,6 +111,7 @@ from superset.tasks.thumbnails import (
)
from superset.tasks.utils import get_current_user
from superset.utils import json
from superset.utils.core import parse_boolean_string
from superset.utils.pdf import build_pdf_from_screenshots
from superset.utils.screenshots import (
DashboardScreenshot,
@ -187,6 +191,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"cache_dashboard_screenshot",
"screenshot",
"put_filters",
"put_colors",
}
resource_name = "dashboard"
allow_browser_login = True
@ -275,6 +280,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
add_model_schema = DashboardPostSchema()
edit_model_schema = DashboardPutSchema()
update_filters_model_schema = DashboardNativeFiltersConfigUpdateSchema()
update_colors_model_schema = DashboardColorsConfigUpdateSchema()
chart_entity_response_schema = ChartEntityResponseSchema()
dashboard_get_response_schema = DashboardGetResponseSchema()
dashboard_dataset_schema = DashboardDatasetSchema()
@ -767,6 +773,88 @@ class DashboardRestApi(BaseSupersetModelRestApi):
response = self.response_422(message=str(ex))
return response
@expose("/<pk>/colors", methods=("PUT",))
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put_colors",
log_to_statsd=False,
)
@requires_json
def put_colors(self, pk: int) -> Response:
"""
Modify colors configuration for a dashboard.
---
put:
summary: Update colors configuration for a dashboard.
parameters:
- in: path
schema:
type: integer
name: pk
- in: query
name: mark_updated
schema:
type: boolean
description: Whether to update the dashboard changed_on field
requestBody:
description: Colors configuration
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardColorsConfigUpdateSchema'
responses:
200:
description: Dashboard colors updated
content:
application/json:
schema:
type: object
properties:
result:
type: array
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404:
$ref: '#/components/responses/404'
422:
$ref: '#/components/responses/422'
500:
$ref: '#/components/responses/500'
"""
try:
item = self.update_colors_model_schema.load(request.json, partial=True)
except ValidationError as error:
return self.response_400(message=error.messages)
try:
mark_updated = parse_boolean_string(
request.args.get("mark_updated", "true")
)
UpdateDashboardColorsConfigCommand(pk, item, mark_updated).run()
response = self.response(200)
except DashboardNotFoundError:
response = self.response_404()
except DashboardForbiddenError:
response = self.response_403()
except DashboardInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except DashboardColorsConfigUpdateFailedError as ex:
logger.error(
"Error changing color configuration for dashboard %s: %s",
self.__class__.__name__,
str(ex),
exc_info=True,
)
response = self.response_422(message=str(ex))
return response
@expose("/<pk>", methods=("DELETE",))
@protect()
@safe
@ -1174,6 +1262,11 @@ class DashboardRestApi(BaseSupersetModelRestApi):
schema:
type: string
name: digest
- in: query
name: download_format
schema:
type: string
enum: [png, pdf]
responses:
200:
description: Dashboard thumbnail image

View File

@ -428,6 +428,15 @@ class DashboardNativeFiltersConfigUpdateSchema(BaseDashboardSchema):
reordered = fields.List(fields.String(), allow_none=False)
class DashboardColorsConfigUpdateSchema(BaseDashboardSchema):
color_namespace = fields.String(allow_none=True)
color_scheme = fields.String(allow_none=True)
map_label_colors = fields.Dict(allow_none=False)
shared_label_colors = SharedLabelsColorsField()
label_colors = fields.Dict(allow_none=False)
color_scheme_domain = fields.List(fields.String(), allow_none=False)
class DashboardScreenshotPostSchema(Schema):
dataMask = fields.Dict(
keys=fields.Str(),

View File

@ -3197,3 +3197,114 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
response = self._cache_screenshot(dashboard.id)
assert response.status_code == 404
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_put_dashboard_colors(self):
"""
Dashboard API: Test updating dashboard colors
"""
self.login(ADMIN_USERNAME)
dashboard = Dashboard.get("world_health")
colors = {
"label_colors": {"Sales": "#FF0000", "Profit": "#00FF00"},
"shared_label_colors": ["#0000FF", "#FFFF00"],
"map_label_colors": {"Revenue": "#FFFFFF"},
"color_scheme": "d3Category10",
}
uri = f"api/v1/dashboard/{dashboard.id}/colors"
rv = self.client.put(uri, json=colors)
assert rv.status_code == 200
updated_dashboard = db.session.query(Dashboard).get(dashboard.id)
updated_label_colors = json.loads(updated_dashboard.json_metadata).get(
"label_colors"
)
updated_shared_label_colors = json.loads(updated_dashboard.json_metadata).get(
"shared_label_colors"
)
updated_map_label_colors = json.loads(updated_dashboard.json_metadata).get(
"map_label_colors"
)
updated_color_scheme = json.loads(updated_dashboard.json_metadata).get(
"color_scheme"
)
assert updated_label_colors == colors["label_colors"]
assert updated_shared_label_colors == colors["shared_label_colors"]
assert updated_map_label_colors == colors["map_label_colors"]
assert updated_color_scheme == colors["color_scheme"]
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_put_dashboard_colors_no_mark_updated(self):
"""
Dashboard API: Test updating dashboard colors without marking the dashboard as updated
"""
self.login(ADMIN_USERNAME)
dashboard = Dashboard.get("world_health")
colors = {"color_scheme": "d3Category10"}
previous_changed_on = dashboard.changed_on
uri = f"api/v1/dashboard/{dashboard.id}/colors?mark_updated=false"
rv = self.client.put(uri, json=colors)
assert rv.status_code == 200
updated_dashboard = db.session.query(Dashboard).get(dashboard.id)
updated_color_scheme = json.loads(updated_dashboard.json_metadata).get(
"color_scheme"
)
assert updated_color_scheme == colors["color_scheme"]
assert updated_dashboard.changed_on == previous_changed_on
def test_put_dashboard_colors_not_found(self):
"""
Dashboard API: Test updating colors for dashboard that does not exist
"""
self.login(ADMIN_USERNAME)
colors = {"label_colors": {"Sales": "#FF0000"}}
invalid_id = self.get_nonexistent_numeric_id(Dashboard)
uri = f"api/v1/dashboard/{invalid_id}/colors"
rv = self.client.put(uri, json=colors)
assert rv.status_code == 404
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_put_dashboard_colors_invalid(self):
"""
Dashboard API: Test updating dashboard colors with invalid color format
"""
self.login(ADMIN_USERNAME)
dashboard = Dashboard.get("world_health")
colors = {"test_invalid_prop": {"Sales": "invalid"}}
uri = f"api/v1/dashboard/{dashboard.id}/colors"
rv = self.client.put(uri, json=colors)
assert rv.status_code == 400
def test_put_dashboard_colors_not_authorized(self):
"""
Dashboard API: Test updating colors without authorization
"""
with self.create_app().app_context():
admin = security_manager.find_user("admin")
dashboard = self.insert_dashboard("title", None, [admin.id])
assert dashboard.id is not None
colors = {"label_colors": {"Sales": "#FF0000"}}
self.login(GAMMA_USERNAME)
uri = f"api/v1/dashboard/{dashboard.id}/colors"
rv = self.client.put(uri, json=colors)
assert rv.status_code == 403
yield dashboard
# Cleanup
db.session.delete(dashboard)
db.session.commit()