mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-27 13:49:10 +02:00
Fix/variant hashing pg10compat (#5218)
Co-authored-by: Ivar Conradi Østhus <ivar@getunleash.io> Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> Co-authored-by: Simon Hornby <liquidwicked64@gmail.com>
This commit is contained in:
parent
1e613d8224
commit
264b32be79
@ -53,8 +53,6 @@ const UsersList = () => {
|
|||||||
const [delUser, setDelUser] = useState<IUser>();
|
const [delUser, setDelUser] = useState<IUser>();
|
||||||
const { planUsers, isBillingUsers } = useUsersPlan(users);
|
const { planUsers, isBillingUsers } = useUsersPlan(users);
|
||||||
|
|
||||||
const accessOverviewEnabled = useUiFlag('accessOverview');
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@ -271,26 +269,15 @@ const UsersList = () => {
|
|||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider />
|
<PageHeader.Divider />
|
||||||
|
<Tooltip
|
||||||
<ConditionallyRender
|
title='Exports user access information'
|
||||||
condition={
|
arrow
|
||||||
isEnterprise() &&
|
describeChild
|
||||||
Boolean(accessOverviewEnabled)
|
>
|
||||||
}
|
<IconButton onClick={downloadCSV}>
|
||||||
show={() => (
|
<Download />
|
||||||
<>
|
</IconButton>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
title='Exports user access information'
|
|
||||||
arrow
|
|
||||||
describeChild
|
|
||||||
>
|
|
||||||
<IconButton onClick={downloadCSV}>
|
|
||||||
<Download />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
color='primary'
|
color='primary'
|
||||||
|
@ -65,7 +65,6 @@ export type UiFlags = {
|
|||||||
doraMetrics?: boolean;
|
doraMetrics?: boolean;
|
||||||
variantTypeNumber?: boolean;
|
variantTypeNumber?: boolean;
|
||||||
privateProjects?: boolean;
|
privateProjects?: boolean;
|
||||||
accessOverview?: boolean;
|
|
||||||
dependentFeatures?: boolean;
|
dependentFeatures?: boolean;
|
||||||
banners?: boolean;
|
banners?: boolean;
|
||||||
disableEnvsOnRevive?: boolean;
|
disableEnvsOnRevive?: boolean;
|
||||||
|
@ -139,7 +139,6 @@
|
|||||||
"knex": "^2.5.1",
|
"knex": "^2.5.1",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.groupby": "^4.6.0",
|
"lodash.groupby": "^4.6.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"lodash.sortby": "^4.7.0",
|
"lodash.sortby": "^4.7.0",
|
||||||
"log4js": "^6.0.0",
|
"log4js": "^6.0.0",
|
||||||
"make-fetch-happen": "^11.0.0",
|
"make-fetch-happen": "^11.0.0",
|
||||||
|
@ -71,7 +71,6 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"accessOverview": false,
|
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"banners": false,
|
"banners": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
import sortBy from 'lodash.sortby';
|
|
||||||
|
|
||||||
interface Difference {
|
|
||||||
index: (string | number)[];
|
|
||||||
reason: string;
|
|
||||||
valueA: any;
|
|
||||||
valueB: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deepDiff(arr1: any[], arr2: any[]): Difference[] | null {
|
|
||||||
const diff: Difference[] = [];
|
|
||||||
|
|
||||||
function compare(a: any, b: any, parentIndex: (string | number)[]): void {
|
|
||||||
if (Array.isArray(a) && Array.isArray(b)) {
|
|
||||||
if (a.length !== b.length) {
|
|
||||||
diff.push({
|
|
||||||
index: parentIndex,
|
|
||||||
reason: 'Different lengths',
|
|
||||||
valueA: a,
|
|
||||||
valueB: b,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const sortedA = sortBy(a, 'name');
|
|
||||||
const sortedB = sortBy(b, 'name');
|
|
||||||
|
|
||||||
for (let i = 0; i < sortedA.length; i++) {
|
|
||||||
compare(sortedA[i], sortedB[i], parentIndex.concat(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
typeof a === 'object' &&
|
|
||||||
a !== null &&
|
|
||||||
typeof b === 'object' &&
|
|
||||||
b !== null
|
|
||||||
) {
|
|
||||||
const keysA = Object.keys(a);
|
|
||||||
const keysB = Object.keys(b);
|
|
||||||
|
|
||||||
if (
|
|
||||||
keysA.length !== keysB.length ||
|
|
||||||
!keysA.every((key) => keysB.includes(key))
|
|
||||||
) {
|
|
||||||
diff.push({
|
|
||||||
index: parentIndex,
|
|
||||||
reason: 'Different keys',
|
|
||||||
valueA: a,
|
|
||||||
valueB: b,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for (const key of keysA) {
|
|
||||||
compare(a[key], b[key], parentIndex.concat(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (a !== b) {
|
|
||||||
diff.push({
|
|
||||||
index: parentIndex,
|
|
||||||
reason: 'Different values',
|
|
||||||
valueA: a,
|
|
||||||
valueB: b,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compare(arr1, arr2, []);
|
|
||||||
|
|
||||||
return diff.length > 0 ? diff : null;
|
|
||||||
}
|
|
@ -102,8 +102,6 @@ import { IPrivateProjectChecker } from '../private-project/privateProjectChecker
|
|||||||
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
||||||
import EventService from '../../services/event-service';
|
import EventService from '../../services/event-service';
|
||||||
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
||||||
import isEqual from 'lodash.isequal';
|
|
||||||
import { deepDiff } from './deep-diff';
|
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -1058,22 +1056,6 @@ class FeatureToggleService {
|
|||||||
await this.featureToggleStore.getPlaygroundFeatures(query),
|
await this.featureToggleStore.getPlaygroundFeatures(query),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const equal = isEqual(
|
|
||||||
featuresFromClientStore,
|
|
||||||
featuresFromFeatureToggleStore,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!equal) {
|
|
||||||
const difference = deepDiff(
|
|
||||||
featuresFromClientStore,
|
|
||||||
featuresFromFeatureToggleStore,
|
|
||||||
);
|
|
||||||
this.logger.warn(
|
|
||||||
'getPlaygroundFeatures: features from client-feature-toggle-store is not equal to features from feature-toggle-store',
|
|
||||||
difference,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
||||||
? featuresFromFeatureToggleStore
|
? featuresFromFeatureToggleStore
|
||||||
: featuresFromClientStore;
|
: featuresFromClientStore;
|
||||||
@ -1109,22 +1091,6 @@ class FeatureToggleService {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const equal = isEqual(
|
|
||||||
featuresFromClientStore,
|
|
||||||
featuresFromFeatureToggleStore,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!equal) {
|
|
||||||
const difference = deepDiff(
|
|
||||||
featuresFromClientStore,
|
|
||||||
featuresFromFeatureToggleStore,
|
|
||||||
);
|
|
||||||
this.logger.warn(
|
|
||||||
'getFeatureToggles: features from client-feature-toggle-store is not equal to features from feature-toggle-store diff',
|
|
||||||
difference,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
||||||
? featuresFromFeatureToggleStore
|
? featuresFromFeatureToggleStore
|
||||||
: featuresFromClientStore;
|
: featuresFromClientStore;
|
||||||
|
@ -342,11 +342,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||||
query.leftJoin(
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
'last_seen_at_metrics',
|
this.on(
|
||||||
'last_seen_at_metrics.environment',
|
'last_seen_at_metrics.environment',
|
||||||
'features_view.environment_name',
|
'=',
|
||||||
);
|
'features_view.environment_name',
|
||||||
|
).andOn(
|
||||||
|
'last_seen_at_metrics.feature_name',
|
||||||
|
'=',
|
||||||
|
'features_view.name',
|
||||||
|
);
|
||||||
|
});
|
||||||
// Override feature view for now
|
// Override feature view for now
|
||||||
selectColumns.push(
|
selectColumns.push(
|
||||||
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
import { deepDiff } from '../deep-diff'; // Import the deepDiff function
|
|
||||||
|
|
||||||
describe('deepDiff', () => {
|
|
||||||
test('should sort arrays by name before comparing', () => {
|
|
||||||
// Define two arrays that are identical except for the order of elements
|
|
||||||
const array1 = [
|
|
||||||
{ name: 'b', value: 2 },
|
|
||||||
{ name: 'a', value: 1 },
|
|
||||||
];
|
|
||||||
const array2 = [
|
|
||||||
{ name: 'a', value: 1 },
|
|
||||||
{ name: 'b', value: 2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// If the function correctly sorts before comparing, there should be no differences
|
|
||||||
const result = deepDiff(array1, array2);
|
|
||||||
|
|
||||||
// Assert that there is no difference
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for equal arrays', () => {
|
|
||||||
const arr1 = [1, 2, 3];
|
|
||||||
const arr2 = [1, 2, 3];
|
|
||||||
expect(deepDiff(arr1, arr2)).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should find differences in arrays with different lengths', () => {
|
|
||||||
const arr1 = [1, 2, 3];
|
|
||||||
const arr2 = [1, 2, 3, 4];
|
|
||||||
expect(deepDiff(arr1, arr2)).toEqual([
|
|
||||||
{
|
|
||||||
index: [],
|
|
||||||
reason: 'Different lengths',
|
|
||||||
valueA: arr1,
|
|
||||||
valueB: arr2,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should find differences in arrays with different values', () => {
|
|
||||||
const arr1 = [1, 2, 3];
|
|
||||||
const arr2 = [1, 4, 3];
|
|
||||||
expect(deepDiff(arr1, arr2)).toEqual([
|
|
||||||
{
|
|
||||||
index: [1],
|
|
||||||
reason: 'Different values',
|
|
||||||
valueA: 2,
|
|
||||||
valueB: 4,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should find differences in arrays with different keys in objects', () => {
|
|
||||||
const arr1 = [{ a: 1 }, { b: 2 }];
|
|
||||||
const arr2 = [{ a: 1 }, { c: 2 }];
|
|
||||||
expect(deepDiff(arr1, arr2)).toEqual([
|
|
||||||
{
|
|
||||||
index: [1],
|
|
||||||
reason: 'Different keys',
|
|
||||||
valueA: { b: 2 },
|
|
||||||
valueB: { c: 2 },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle nested differences in objects', () => {
|
|
||||||
const arr1 = [{ a: { b: 1 } }, { c: { d: 2 } }];
|
|
||||||
const arr2 = [{ a: { b: 1 } }, { c: { d: 3 } }];
|
|
||||||
expect(deepDiff(arr1, arr2)).toEqual([
|
|
||||||
{
|
|
||||||
index: [1, 'c', 'd'],
|
|
||||||
reason: 'Different values',
|
|
||||||
valueA: 2,
|
|
||||||
valueB: 3,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
@ -154,3 +154,53 @@ test('response should include last seen at per environment for multiple environm
|
|||||||
expect(production.name).toBe('production');
|
expect(production.name).toBe('production');
|
||||||
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('response should include last seen at per environment correctly for a single toggle /api/admin/project/:projectId/features/:featureName', async () => {
|
||||||
|
const featureName = 'multiple-environment-last-seen-at-single-toggle';
|
||||||
|
await app.createFeature(featureName);
|
||||||
|
await setupLastSeenAtTest(`${featureName}1`);
|
||||||
|
await setupLastSeenAtTest(`${featureName}2`);
|
||||||
|
await setupLastSeenAtTest(`${featureName}3`);
|
||||||
|
await setupLastSeenAtTest(`${featureName}4`);
|
||||||
|
await setupLastSeenAtTest(`${featureName}5`);
|
||||||
|
|
||||||
|
await insertLastSeenAt(
|
||||||
|
featureName,
|
||||||
|
db.rawDatabase,
|
||||||
|
'default',
|
||||||
|
'2023-08-01 12:30:56',
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertLastSeenAt(
|
||||||
|
featureName,
|
||||||
|
db.rawDatabase,
|
||||||
|
'development',
|
||||||
|
'2023-08-01 12:30:56',
|
||||||
|
);
|
||||||
|
|
||||||
|
await insertLastSeenAt(
|
||||||
|
featureName,
|
||||||
|
db.rawDatabase,
|
||||||
|
'production',
|
||||||
|
'2023-08-01 12:30:56',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.get(`/api/admin/projects/default/features/${featureName}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body.environments).toMatchObject([
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'development',
|
||||||
|
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'production',
|
||||||
|
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
@ -26,7 +26,6 @@ export type IFlagKey =
|
|||||||
| 'featureNamingPattern'
|
| 'featureNamingPattern'
|
||||||
| 'doraMetrics'
|
| 'doraMetrics'
|
||||||
| 'variantTypeNumber'
|
| 'variantTypeNumber'
|
||||||
| 'accessOverview'
|
|
||||||
| 'privateProjects'
|
| 'privateProjects'
|
||||||
| 'dependentFeatures'
|
| 'dependentFeatures'
|
||||||
| 'disableMetrics'
|
| 'disableMetrics'
|
||||||
@ -137,10 +136,6 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
accessOverview: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
disableMetrics: parseEnvVarBoolean(
|
disableMetrics: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
||||||
false,
|
false,
|
||||||
|
@ -7,7 +7,7 @@ exports.up = function(db, cb) {
|
|||||||
PRIMARY KEY (day, environment)
|
PRIMARY KEY (day, environment)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE FUNCTION unleash_update_stat_environment_changes_counter() RETURNS trigger AS $unleash_update_changes_counter$
|
CREATE OR REPLACE FUNCTION unleash_update_stat_environment_changes_counter() RETURNS trigger AS $unleash_update_changes_counter$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.environment IS NOT NULL THEN
|
IF NEW.environment IS NOT NULL THEN
|
||||||
INSERT INTO stat_environment_updates(day, environment, updates) SELECT DATE_TRUNC('Day', NEW.created_at), NEW.environment, 1 ON CONFLICT (day, environment) DO UPDATE SET updates = stat_environment_updates.updates + 1;
|
INSERT INTO stat_environment_updates(day, environment, updates) SELECT DATE_TRUNC('Day', NEW.created_at), NEW.environment, 1 ON CONFLICT (day, environment) DO UPDATE SET updates = stat_environment_updates.updates + 1;
|
||||||
@ -19,7 +19,7 @@ exports.up = function(db, cb) {
|
|||||||
|
|
||||||
CREATE TRIGGER unleash_update_stat_environment_changes
|
CREATE TRIGGER unleash_update_stat_environment_changes
|
||||||
AFTER INSERT ON events
|
AFTER INSERT ON events
|
||||||
FOR EACH ROW EXECUTE FUNCTION unleash_update_stat_environment_changes_counter();
|
FOR EACH ROW EXECUTE PROCEDURE unleash_update_stat_environment_changes_counter();
|
||||||
`, cb);
|
`, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ process.nextTick(async () => {
|
|||||||
doraMetrics: true,
|
doraMetrics: true,
|
||||||
variantTypeNumber: true,
|
variantTypeNumber: true,
|
||||||
privateProjects: true,
|
privateProjects: true,
|
||||||
accessOverview: true,
|
|
||||||
dependentFeatures: true,
|
dependentFeatures: true,
|
||||||
useLastSeenRefactor: true,
|
useLastSeenRefactor: true,
|
||||||
disableEnvsOnRevive: true,
|
disableEnvsOnRevive: true,
|
||||||
|
@ -326,10 +326,15 @@ export const insertLastSeenAt = async (
|
|||||||
environment: string = 'default',
|
environment: string = 'default',
|
||||||
date: string = '2023-10-01 12:34:56',
|
date: string = '2023-10-01 12:34:56',
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
try {
|
||||||
|
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
||||||
VALUES ('${featureName}', '${environment}', '${date}');`);
|
VALUES ('${featureName}', '${environment}', '${date}');`);
|
||||||
|
|
||||||
return date;
|
return date;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return Promise.resolve('');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertFeatureEnvironmentsLastSeen = async (
|
export const insertFeatureEnvironmentsLastSeen = async (
|
||||||
|
Loading…
Reference in New Issue
Block a user