mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: backend for retrieving tag colors (#9610)
Add backend for retrieving tag colors
This commit is contained in:
		
							parent
							
								
									9106fbf721
								
							
						
					
					
						commit
						7d7a949093
					
				@ -102,11 +102,18 @@ class FeatureTagStore implements IFeatureTagStore {
 | 
				
			|||||||
        const stopTimer = this.timer('getAllForFeature');
 | 
					        const stopTimer = this.timer('getAllForFeature');
 | 
				
			||||||
        if (await this.featureExists(featureName)) {
 | 
					        if (await this.featureExists(featureName)) {
 | 
				
			||||||
            const rows = await this.db
 | 
					            const rows = await this.db
 | 
				
			||||||
                .select(COLUMNS)
 | 
					                .select([...COLUMNS, 'tag_types.color as color'])
 | 
				
			||||||
                .from<FeatureTagTable>(TABLE)
 | 
					                .from<FeatureTagTable>(TABLE)
 | 
				
			||||||
 | 
					                .leftJoin('tag_types', 'tag_types.name', 'feature_tag.tag_type')
 | 
				
			||||||
                .where({ feature_name: featureName });
 | 
					                .where({ feature_name: featureName });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            stopTimer();
 | 
					            stopTimer();
 | 
				
			||||||
            return rows.map(this.featureTagRowToTag);
 | 
					
 | 
				
			||||||
 | 
					            return rows.map((row) => ({
 | 
				
			||||||
 | 
					                type: row.tag_type,
 | 
				
			||||||
 | 
					                value: row.tag_value,
 | 
				
			||||||
 | 
					                color: row.color,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            throw new NotFoundError(
 | 
					            throw new NotFoundError(
 | 
				
			||||||
                `Could not find feature with name ${featureName}`,
 | 
					                `Could not find feature with name ${featureName}`,
 | 
				
			||||||
 | 
				
			|||||||
@ -134,6 +134,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
 | 
				
			|||||||
                    'environments.sort_order as environment_sort_order',
 | 
					                    'environments.sort_order as environment_sort_order',
 | 
				
			||||||
                    'ft.tag_value as tag_value',
 | 
					                    'ft.tag_value as tag_value',
 | 
				
			||||||
                    'ft.tag_type as tag_type',
 | 
					                    'ft.tag_type as tag_type',
 | 
				
			||||||
 | 
					                    'tag_types.color as tag_type_color',
 | 
				
			||||||
                    'segments.name as segment_name',
 | 
					                    'segments.name as segment_name',
 | 
				
			||||||
                    'users.id as user_id',
 | 
					                    'users.id as user_id',
 | 
				
			||||||
                    'users.name as user_name',
 | 
					                    'users.name as user_name',
 | 
				
			||||||
@ -207,6 +208,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
 | 
				
			|||||||
                        'ft.feature_name',
 | 
					                        'ft.feature_name',
 | 
				
			||||||
                        'features.name',
 | 
					                        'features.name',
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					                    .leftJoin('tag_types', 'tag_types.name', 'ft.tag_type')
 | 
				
			||||||
                    .leftJoin(
 | 
					                    .leftJoin(
 | 
				
			||||||
                        'feature_strategies',
 | 
					                        'feature_strategies',
 | 
				
			||||||
                        'feature_strategies.feature_name',
 | 
					                        'feature_strategies.feature_name',
 | 
				
			||||||
@ -548,6 +550,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
 | 
				
			|||||||
        return {
 | 
					        return {
 | 
				
			||||||
            value: r.tag_value,
 | 
					            value: r.tag_value,
 | 
				
			||||||
            type: r.tag_type,
 | 
					            type: r.tag_type,
 | 
				
			||||||
 | 
					            color: r.tag_type_color,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1252,3 +1252,37 @@ test('should return archived when query param set', async () => {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should return tags with color information from tag type', async () => {
 | 
				
			||||||
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .put('/api/admin/tag-types/simple')
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            name: 'simple',
 | 
				
			||||||
 | 
					            color: '#FF0000',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.addTag('my_feature_a', {
 | 
				
			||||||
 | 
					        type: 'simple',
 | 
				
			||||||
 | 
					        value: 'my_tag',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { body } = await searchFeatures({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(body).toMatchObject({
 | 
				
			||||||
 | 
					        features: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                name: 'my_feature_a',
 | 
				
			||||||
 | 
					                tags: [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        type: 'simple',
 | 
				
			||||||
 | 
					                        value: 'my_tag',
 | 
				
			||||||
 | 
					                        color: '#FF0000',
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import { projectEnvironmentSchema } from './project-environment-schema';
 | 
				
			|||||||
import { createStrategyVariantSchema } from './create-strategy-variant-schema';
 | 
					import { createStrategyVariantSchema } from './create-strategy-variant-schema';
 | 
				
			||||||
import { strategyVariantSchema } from './strategy-variant-schema';
 | 
					import { strategyVariantSchema } from './strategy-variant-schema';
 | 
				
			||||||
import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
 | 
					import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
 | 
				
			||||||
 | 
					import { tagSchema } from './tag-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deprecatedProjectOverviewSchema = {
 | 
					export const deprecatedProjectOverviewSchema = {
 | 
				
			||||||
    $id: '#/components/schemas/deprecatedProjectOverviewSchema',
 | 
					    $id: '#/components/schemas/deprecatedProjectOverviewSchema',
 | 
				
			||||||
@ -144,6 +145,7 @@ export const deprecatedProjectOverviewSchema = {
 | 
				
			|||||||
            variantSchema,
 | 
					            variantSchema,
 | 
				
			||||||
            projectStatsSchema,
 | 
					            projectStatsSchema,
 | 
				
			||||||
            createFeatureNamingPatternSchema,
 | 
					            createFeatureNamingPatternSchema,
 | 
				
			||||||
 | 
					            tagSchema,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import { projectEnvironmentSchema } from './project-environment-schema';
 | 
				
			|||||||
import { createStrategyVariantSchema } from './create-strategy-variant-schema';
 | 
					import { createStrategyVariantSchema } from './create-strategy-variant-schema';
 | 
				
			||||||
import { strategyVariantSchema } from './strategy-variant-schema';
 | 
					import { strategyVariantSchema } from './strategy-variant-schema';
 | 
				
			||||||
import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
 | 
					import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
 | 
				
			||||||
 | 
					import { tagSchema } from './tag-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const healthOverviewSchema = {
 | 
					export const healthOverviewSchema = {
 | 
				
			||||||
    $id: '#/components/schemas/healthOverviewSchema',
 | 
					    $id: '#/components/schemas/healthOverviewSchema',
 | 
				
			||||||
@ -138,6 +139,7 @@ export const healthOverviewSchema = {
 | 
				
			|||||||
            variantSchema,
 | 
					            variantSchema,
 | 
				
			||||||
            projectStatsSchema,
 | 
					            projectStatsSchema,
 | 
				
			||||||
            createFeatureNamingPatternSchema,
 | 
					            createFeatureNamingPatternSchema,
 | 
				
			||||||
 | 
					            tagSchema,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,13 @@ export const tagSchema = {
 | 
				
			|||||||
                'The [type](https://docs.getunleash.io/reference/feature-toggles#tags) of the tag',
 | 
					                'The [type](https://docs.getunleash.io/reference/feature-toggles#tags) of the tag',
 | 
				
			||||||
            example: 'simple',
 | 
					            example: 'simple',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        color: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            description: 'The hexadecimal color code for the tag type.',
 | 
				
			||||||
 | 
					            example: '#FFFFFF',
 | 
				
			||||||
 | 
					            pattern: '^#[0-9A-Fa-f]{6}$',
 | 
				
			||||||
 | 
					            nullable: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    components: {},
 | 
					    components: {},
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
				
			|||||||
@ -355,6 +355,7 @@ export interface IFeatureToggleDeltaQuery extends IFeatureToggleQuery {
 | 
				
			|||||||
export interface ITag {
 | 
					export interface ITag {
 | 
				
			||||||
    value: string;
 | 
					    value: string;
 | 
				
			||||||
    type: string;
 | 
					    type: string;
 | 
				
			||||||
 | 
					    color?: string | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAddonParameterDefinition {
 | 
					export interface IAddonParameterDefinition {
 | 
				
			||||||
 | 
				
			|||||||
@ -10,13 +10,17 @@ let db: ITestDb;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
    db = await dbInit('tag_api_serial', getLogger);
 | 
					    db = await dbInit('tag_api_serial', getLogger);
 | 
				
			||||||
    app = await setupAppWithCustomConfig(db.stores, {
 | 
					    app = await setupAppWithCustomConfig(
 | 
				
			||||||
        experimental: {
 | 
					        db.stores,
 | 
				
			||||||
            flags: {
 | 
					        {
 | 
				
			||||||
                strictSchemaValidation: true,
 | 
					            experimental: {
 | 
				
			||||||
 | 
					                flags: {
 | 
				
			||||||
 | 
					                    strictSchemaValidation: true,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    });
 | 
					        db.rawDatabase,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
afterAll(async () => {
 | 
					afterAll(async () => {
 | 
				
			||||||
@ -219,3 +223,47 @@ test('backward compatibility: the API should return invalid tag names if they ex
 | 
				
			|||||||
    const { body } = await app.request.get('/api/admin/tags').expect(200);
 | 
					    const { body } = await app.request.get('/api/admin/tags').expect(200);
 | 
				
			||||||
    expect(body.tags).toContainEqual(tag);
 | 
					    expect(body.tags).toContainEqual(tag);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should include tag color information when getting feature tags', async () => {
 | 
				
			||||||
 | 
					    const featureName = 'test.feature.with.color';
 | 
				
			||||||
 | 
					    const tagType = 'simple';
 | 
				
			||||||
 | 
					    const tag = {
 | 
				
			||||||
 | 
					        value: 'TeamRed',
 | 
				
			||||||
 | 
					        type: tagType,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.request.post('/api/admin/projects/default/features').send({
 | 
				
			||||||
 | 
					        name: featureName,
 | 
				
			||||||
 | 
					        type: 'kill-switch',
 | 
				
			||||||
 | 
					        enabled: true,
 | 
				
			||||||
 | 
					        strategies: [{ name: 'default' }],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .put(`/api/admin/tag-types/${tagType}`)
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            name: tagType,
 | 
				
			||||||
 | 
					            color: '#FF0000',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .put(`/api/admin/features/${featureName}/tags`)
 | 
				
			||||||
 | 
					        .send({ addedTags: [tag], removedTags: [] })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { body } = await app.request
 | 
				
			||||||
 | 
					        .get(`/api/admin/features/${featureName}/tags`)
 | 
				
			||||||
 | 
					        .expect('Content-Type', /json/)
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(body).toMatchObject({
 | 
				
			||||||
 | 
					        tags: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                value: 'TeamRed',
 | 
				
			||||||
 | 
					                type: 'simple',
 | 
				
			||||||
 | 
					                color: '#FF0000',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ let featureToggleStore: IFeatureToggleStore;
 | 
				
			|||||||
const featureName = 'test-tag';
 | 
					const featureName = 'test-tag';
 | 
				
			||||||
const tag = { type: 'simple', value: 'test' };
 | 
					const tag = { type: 'simple', value: 'test' };
 | 
				
			||||||
const TESTUSERID = 3333;
 | 
					const TESTUSERID = 3333;
 | 
				
			||||||
 | 
					const DEFAULT_TAG_COLOR = '#FFFFFF';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
    db = await dbInit('feature_tag_store_serial', getLogger);
 | 
					    db = await dbInit('feature_tag_store_serial', getLogger);
 | 
				
			||||||
@ -45,7 +46,7 @@ test('should tag feature', async () => {
 | 
				
			|||||||
        createdByUserId: TESTUSERID,
 | 
					        createdByUserId: TESTUSERID,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    expect(featureTags).toHaveLength(1);
 | 
					    expect(featureTags).toHaveLength(1);
 | 
				
			||||||
    expect(featureTags[0]).toStrictEqual(tag);
 | 
					    expect(featureTags[0]).toStrictEqual({ ...tag, color: DEFAULT_TAG_COLOR });
 | 
				
			||||||
    expect(featureTag!.featureName).toBe(featureName);
 | 
					    expect(featureTag!.featureName).toBe(featureName);
 | 
				
			||||||
    expect(featureTag!.tagValue).toBe(tag.value);
 | 
					    expect(featureTag!.tagValue).toBe(tag.value);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user