1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

Merge remote-tracking branch 'origin/5.6' into 5.6

This commit is contained in:
Gastón Fournier 2023-11-21 16:21:14 +01:00
commit c9a3f33b1c
No known key found for this signature in database
GPG Key ID: AF45428626E17A8E
35 changed files with 988 additions and 363 deletions

View File

@ -2,6 +2,517 @@
All notable changes to this project will be documented in this file.
## [5.6.8] - 2023-11-16
### Bug Fixes
- Handle concurrent service account updates ([#5349](https://github.com/Unleash/unleash/issues/5349)) ([#5350](https://github.com/Unleash/unleash/issues/5350))
- Take into account project segments permission in form ([#5352](https://github.com/Unleash/unleash/issues/5352)) ([#5354](https://github.com/Unleash/unleash/issues/5354))
## [5.6.7] - 2023-11-15
### Miscellaneous Tasks
- Log unerlying DB error in set user root role ([#5324](https://github.com/Unleash/unleash/issues/5324)) ([#5339](https://github.com/Unleash/unleash/issues/5339))
## [5.6.5] - 2023-11-09
### Bug Fixes
- Segment project fetch when global ([#5313](https://github.com/Unleash/unleash/issues/5313))
## [5.6.4] - 2023-11-03
### Bug Fixes
- Last seen deadlocks ([#5264](https://github.com/Unleash/unleash/issues/5264)) ([#5266](https://github.com/Unleash/unleash/issues/5266))
## [5.6.1] - 2023-10-30
### Miscellaneous Tasks
- Changelog into 5.6 ([#5171](https://github.com/Unleash/unleash/issues/5171))
## [5.6.0] - 2023-10-26
### Bug Fixes
- Account for array length ([#4849](https://github.com/Unleash/unleash/issues/4849))
- Version checker update needs permissions to write id-token
- Partial index on events announced ([#4856](https://github.com/Unleash/unleash/issues/4856))
- Permissions in the role payload ([#4861](https://github.com/Unleash/unleash/issues/4861))
- Add condition for getting max revision id from store ([#4549](https://github.com/Unleash/unleash/issues/4549))
- Update dependency joi to v17.10.2 ([#4883](https://github.com/Unleash/unleash/issues/4883))
- Update dependency db-migrate-pg to v1.5.2 ([#4894](https://github.com/Unleash/unleash/issues/4894))
- Update docusaurus monorepo to v2.4.3 ([#4895](https://github.com/Unleash/unleash/issues/4895))
- Separate project and project enterprise settings forms ([#4911](https://github.com/Unleash/unleash/issues/4911))
- Yarn lint:fix ([#4917](https://github.com/Unleash/unleash/issues/4917))
- Update potentially-stale status dynamically ([#4905](https://github.com/Unleash/unleash/issues/4905))
- ReportTable status column not updating ([#4924](https://github.com/Unleash/unleash/issues/4924))
- Linting ([#4925](https://github.com/Unleash/unleash/issues/4925))
- Only delete SSO-synced group membership where membership was added by SSO sync ([#4929](https://github.com/Unleash/unleash/issues/4929))
- Make cypress list length checks more relaxed ([#4933](https://github.com/Unleash/unleash/issues/4933))
- Remove console from FeatureToggleSwitch ([#4928](https://github.com/Unleash/unleash/issues/4928))
- Remove the info from the variants page ([#4937](https://github.com/Unleash/unleash/issues/4937))
- Change broken link to groups documentation ([#4941](https://github.com/Unleash/unleash/issues/4941))
- Local linter did not find formatting error ([#4954](https://github.com/Unleash/unleash/issues/4954))
- Fail when format or lint is incorrect ([#4956](https://github.com/Unleash/unleash/issues/4956))
- Ignore errors on changelog generation and include token ([#4926](https://github.com/Unleash/unleash/issues/4926))
- Typo in enabled event ([#4960](https://github.com/Unleash/unleash/issues/4960))
- Refactor getProjectOverview store method ([#4972](https://github.com/Unleash/unleash/issues/4972))
- Added await to getActiveUsers tests
- Export NotFoundError and ISegmentService in internals.ts ([#4997](https://github.com/Unleash/unleash/issues/4997))
- Missing uiFlag newInviteLink ([#5000](https://github.com/Unleash/unleash/issues/5000))
- Enable segment importing for oss ([#5010](https://github.com/Unleash/unleash/issues/5010))
- Message banner internal link assumption ([#5011](https://github.com/Unleash/unleash/issues/5011))
- Message banner zIndex ([#5015](https://github.com/Unleash/unleash/issues/5015))
- Error icon, add only relevant variants ([#5014](https://github.com/Unleash/unleash/issues/5014))
- Import segment test and fix ([#5017](https://github.com/Unleash/unleash/issues/5017))
- Disable all environments when reviving a feature ([#4999](https://github.com/Unleash/unleash/issues/4999))
- Maintenance banner should show right away when toggled ([#5021](https://github.com/Unleash/unleash/issues/5021))
- Use correct flag name ([#5026](https://github.com/Unleash/unleash/issues/5026))
- Feature flag playground features in new store ([#5013](https://github.com/Unleash/unleash/issues/5013))
- Small adjustments on the new header icons ([#5043](https://github.com/Unleash/unleash/issues/5043))
- Update dependency nodemailer to v6.9.6 ([#5049](https://github.com/Unleash/unleash/issues/5049))
- Extract username from user should not return undefined ([#5061](https://github.com/Unleash/unleash/issues/5061))
- Log diff ([#5072](https://github.com/Unleash/unleash/issues/5072))
- Server-side request forgery in @cypress/request@2.88.12 ([#5077](https://github.com/Unleash/unleash/issues/5077))
- Correctly set baseUriPath in setupAppWithBaseUrl ([#5068](https://github.com/Unleash/unleash/issues/5068))
- Update failing snapshot
- Add sort to deep diff ([#5084](https://github.com/Unleash/unleash/issues/5084))
- Force deletion of archived toggles when deleting a project ([#5080](https://github.com/Unleash/unleash/issues/5080))
- Add project filter to feature-toggle-list-builder ([#5099](https://github.com/Unleash/unleash/issues/5099))
- Remove docusaurus from main package json ([#5107](https://github.com/Unleash/unleash/issues/5107))
- Project overview refactor flag ([#5110](https://github.com/Unleash/unleash/issues/5110))
- Don't clean up settings when optional data is not present ([#5118](https://github.com/Unleash/unleash/issues/5118))
- One of our deps breaks on node 21 ([#5122](https://github.com/Unleash/unleash/issues/5122))
- Draft banner zIndex ([#5124](https://github.com/Unleash/unleash/issues/5124))
- Wait for bulk archive button to become enabled ([#5121](https://github.com/Unleash/unleash/issues/5121))
- Grey out text and icons for disabled strategies in playground ([#5113](https://github.com/Unleash/unleash/issues/5113))
- Read project id in edit project ([#5134](https://github.com/Unleash/unleash/issues/5134))
- Fix copy functionality always being disabled
- Fix linting for copyfeature ([#5138](https://github.com/Unleash/unleash/issues/5138))
- Last seen at rendering logic ([#5136](https://github.com/Unleash/unleash/issues/5136))
- Only get rows for toggles in project ([#5141](https://github.com/Unleash/unleash/issues/5141))
- Project mode can not be set to null anymore ([#5145](https://github.com/Unleash/unleash/issues/5145))
- Fix broken edit project link ([#5147](https://github.com/Unleash/unleash/issues/5147))
- Do not track empty strings in playground token input ([#5159](https://github.com/Unleash/unleash/issues/5159))
### Documentation
- Strategy variants video update ([#4854](https://github.com/Unleash/unleash/issues/4854))
- Add video to SDK overview reference ([#4855](https://github.com/Unleash/unleash/issues/4855))
- Rollback docusaurus upgrade so the docs work ([#4965](https://github.com/Unleash/unleash/issues/4965))
- Make videos bigger ([#4980](https://github.com/Unleash/unleash/issues/4980))
- Add a custom_edit_url for sdks and edge/proxy ([#4985](https://github.com/Unleash/unleash/issues/4985))
- Add feature availability troubleshooting guide ([#4989](https://github.com/Unleash/unleash/issues/4989))
- Updated sidebars and added missing doc ID ([#4993](https://github.com/Unleash/unleash/issues/4993))
- Dependent features ([#5058](https://github.com/Unleash/unleash/issues/5058))
- Added Flutter and Next.js Tutorials
### Feat
### Features
- Enterprise project settings ([#4844](https://github.com/Unleash/unleash/issues/4844))
- Read model for dependent features ([#4846](https://github.com/Unleash/unleash/issues/4846))
- Feature admin API returns dependencies and children ([#4848](https://github.com/Unleash/unleash/issues/4848))
- Display dependencies and parents in project details ([#4859](https://github.com/Unleash/unleash/issues/4859))
- Edit and delete dependencies menu ([#4863](https://github.com/Unleash/unleash/issues/4863))
- Events for dependencies ([#4864](https://github.com/Unleash/unleash/issues/4864))
- Biome lint ([#4853](https://github.com/Unleash/unleash/issues/4853))
- Add more events in integrations ([#4815](https://github.com/Unleash/unleash/issues/4815))
- Parent and child info in feature overview header ([#4901](https://github.com/Unleash/unleash/issues/4901))
- Generate orval types with dependent features ([#4902](https://github.com/Unleash/unleash/issues/4902))
- Biome lint frontend ([#4903](https://github.com/Unleash/unleash/issues/4903))
- Update dependency permission ([#4910](https://github.com/Unleash/unleash/issues/4910))
- Prevent delete and archive on parent feature ([#4913](https://github.com/Unleash/unleash/issues/4913))
- Change project with feature dependencies ([#4915](https://github.com/Unleash/unleash/issues/4915))
- Copy feature with parent ([#4918](https://github.com/Unleash/unleash/issues/4918))
- Flag for clone dependencies ([#4922](https://github.com/Unleash/unleash/issues/4922))
- Dependent features in playground ([#4930](https://github.com/Unleash/unleash/issues/4930))
- Allow defining initial admin user as env variable ([#4927](https://github.com/Unleash/unleash/issues/4927))
- Allow to delete dependencies when no orphans ([#4952](https://github.com/Unleash/unleash/issues/4952))
- Render segments changes in feature strategy update event messages ([#4950](https://github.com/Unleash/unleash/issues/4950))
- Orval types with change request for dependencies ([#4961](https://github.com/Unleash/unleash/issues/4961))
- Change request dependency UI ([#4966](https://github.com/Unleash/unleash/issues/4966))
- Do not allow to manage dependencies directly with cr enabled ([#4971](https://github.com/Unleash/unleash/issues/4971))
- Visualize dependencies managment in change requests ([#4978](https://github.com/Unleash/unleash/issues/4978))
- Generate declaration map ([#4981](https://github.com/Unleash/unleash/issues/4981))
- Feature changes counted in new table ([#4958](https://github.com/Unleash/unleash/issues/4958))
- Delete dependnecy button through change request ([#4983](https://github.com/Unleash/unleash/issues/4983))
- Add internalMessageBanner feature flag ([#4990](https://github.com/Unleash/unleash/issues/4990))
- Re-order message banners ([#4995](https://github.com/Unleash/unleash/issues/4995))
- Make invite link more visible ([#4984](https://github.com/Unleash/unleash/issues/4984))
- Multiple external message banners ([#4998](https://github.com/Unleash/unleash/issues/4998))
- Prevent adding dependency to archived or removed parent ([#4987](https://github.com/Unleash/unleash/issues/4987))
- Protect archive feature ([#5003](https://github.com/Unleash/unleash/issues/5003))
- Export dependent feature toggles ([#5007](https://github.com/Unleash/unleash/issues/5007))
- Dynamic icons by adding material symbols font ([#5008](https://github.com/Unleash/unleash/issues/5008))
- Message banners table migration ([#5009](https://github.com/Unleash/unleash/issues/5009))
- Make maintenance banner sticky ([#5016](https://github.com/Unleash/unleash/issues/5016))
- Validate archive dependent features ([#5019](https://github.com/Unleash/unleash/issues/5019))
- Dependencies import validation ([#5023](https://github.com/Unleash/unleash/issues/5023))
- Header invite link tracking ([#5001](https://github.com/Unleash/unleash/issues/5001))
- Verify archive dependent features UI ([#5024](https://github.com/Unleash/unleash/issues/5024))
- Add a dialog when reviving / batch reviving features ([#4988](https://github.com/Unleash/unleash/issues/4988))
- Adds a new design to the header icons ([#5025](https://github.com/Unleash/unleash/issues/5025))
- Remove dependency on archive ([#5040](https://github.com/Unleash/unleash/issues/5040))
- Make maintenance-related 503s more intuitive ([#5018](https://github.com/Unleash/unleash/issues/5018))
- Track add and remove dependencies ([#5041](https://github.com/Unleash/unleash/issues/5041))
- Add playground imrpovements flag ([#5045](https://github.com/Unleash/unleash/issues/5045))
- Add new message banner events ([#5055](https://github.com/Unleash/unleash/issues/5055))
- Show dependencies only when using pro/enterprise or at least on… ([#5052](https://github.com/Unleash/unleash/issues/5052))
- Import dependencies ([#5044](https://github.com/Unleash/unleash/issues/5044))
- Add option to return disabled strategies ([#5059](https://github.com/Unleash/unleash/issues/5059))
- Warn about sdk update with feature dependencies ([#5065](https://github.com/Unleash/unleash/issues/5065))
- Allow selection of text in strategies for contexts ([#5071](https://github.com/Unleash/unleash/issues/5071))
- Dependent features use new transaction mechanism ([#5073](https://github.com/Unleash/unleash/issues/5073))
- Adds rate limiting to metric POST endpoints ([#5075](https://github.com/Unleash/unleash/issues/5075))
- Show disabled strategies in playground ([#5081](https://github.com/Unleash/unleash/issues/5081))
- Default session id in frontend api ([#5083](https://github.com/Unleash/unleash/issues/5083))
- Add message banner API hooks ([#5078](https://github.com/Unleash/unleash/issues/5078))
- Display internal message banners ([#5079](https://github.com/Unleash/unleash/issues/5079))
- Prevent self dependencies ([#5090](https://github.com/Unleash/unleash/issues/5090))
- Check if child and parent are in the same project ([#5093](https://github.com/Unleash/unleash/issues/5093))
- Detect grandchild dependency ([#5094](https://github.com/Unleash/unleash/issues/5094))
- Ensure at least one owner on remove user/group access ([#5085](https://github.com/Unleash/unleash/issues/5085))
- Add new sticky component to handle stacked stickies ([#5088](https://github.com/Unleash/unleash/issues/5088))
- Show warning about dependencies removed on archive ([#5104](https://github.com/Unleash/unleash/issues/5104))
- Add hasStrategies and hasEnabledStrategies on feature environments ([#5012](https://github.com/Unleash/unleash/issues/5012))
- Promise timeout on lock ([#5108](https://github.com/Unleash/unleash/issues/5108))
- Banners admin page ([#5111](https://github.com/Unleash/unleash/issues/5111))
- Add job that cleans last seen every 24 hours ([#5114](https://github.com/Unleash/unleash/issues/5114))
- Make multiple roles per group/user GA by removing the flag ([#5109](https://github.com/Unleash/unleash/issues/5109))
- Replace gravatar-url with inline function ([#5128](https://github.com/Unleash/unleash/issues/5128))
- Improved has children/has parent indicator ([#5135](https://github.com/Unleash/unleash/issues/5135))
- Banner modal ([#5132](https://github.com/Unleash/unleash/issues/5132))
- Feature search stub ([#5143](https://github.com/Unleash/unleash/issues/5143))
- Use new on/off endpoints in banners toggles ([#5144](https://github.com/Unleash/unleash/issues/5144))
- Create db table for cr schedules ([#5148](https://github.com/Unleash/unleash/issues/5148))
- Add feature search service ([#5149](https://github.com/Unleash/unleash/issues/5149))
- Feature search basic functionality ([#5150](https://github.com/Unleash/unleash/issues/5150))
- Add input for api token in playground ([#5130](https://github.com/Unleash/unleash/issues/5130))
- Banner UI/UX adjustments ([#5151](https://github.com/Unleash/unleash/issues/5151))
- Remove feature flag for datadog json template ([#5105](https://github.com/Unleash/unleash/issues/5105))
- Make all internal rate limits configurable ([#5095](https://github.com/Unleash/unleash/issues/5095))
- Token input improvements ([#5155](https://github.com/Unleash/unleash/issues/5155))
- Playground token input usage tracking ([#5157](https://github.com/Unleash/unleash/issues/5157))
- Filter features by type ([#5160](https://github.com/Unleash/unleash/issues/5160))
- Add scheduledConfigurationChanges flag ([#5161](https://github.com/Unleash/unleash/issues/5161))
### Fix
- Copy feature alert when change requests enabled in any env ([#4964](https://github.com/Unleash/unleash/issues/4964))
### Miscellaneous Tasks
- Bump version to 5.6.0 ([#4847](https://github.com/Unleash/unleash/issues/4847))
- Limit the amount of unannounced events we announce ([#4845](https://github.com/Unleash/unleash/issues/4845))
- Update DATABASE_URL to use the database created via POSTGRES_D… ([#4836](https://github.com/Unleash/unleash/issues/4836))
- Unleash users page ([#4687](https://github.com/Unleash/unleash/issues/4687))
- Adds Biome as a recommended extension for vscode ([#4909](https://github.com/Unleash/unleash/issues/4909))
- Use https://git-cliff.org for changelog ([#4907](https://github.com/Unleash/unleash/issues/4907))
- Automate changelog generation on release branch ([#4914](https://github.com/Unleash/unleash/issues/4914))
- Revamp transactional impl ([#4916](https://github.com/Unleash/unleash/issues/4916))
- Handle transactions already started at the controller layer ([#4953](https://github.com/Unleash/unleash/issues/4953))
- Improve UI Config type ([#4959](https://github.com/Unleash/unleash/issues/4959))
- Improve type on import service ([#4962](https://github.com/Unleash/unleash/issues/4962))
- Rename validate step ([#4969](https://github.com/Unleash/unleash/issues/4969))
- Avoid building frontend if not needed ([#4982](https://github.com/Unleash/unleash/issues/4982))
- Split interfaces for import and export ([#5004](https://github.com/Unleash/unleash/issues/5004))
- Add enterprise event ([#5056](https://github.com/Unleash/unleash/issues/5056))
- GA transactional decorator ([#5020](https://github.com/Unleash/unleash/issues/5020))
- Update node sdk to official ga version with dependent flags ([#5042](https://github.com/Unleash/unleash/issues/5042))
- Introduce type to prevent potential issues ([#5066](https://github.com/Unleash/unleash/issues/5066))
- Generate types ([#5074](https://github.com/Unleash/unleash/issues/5074))
- Add splash screen for oss segments ([#5053](https://github.com/Unleash/unleash/issues/5053))
- Remove storybook ([#5091](https://github.com/Unleash/unleash/issues/5091))
- Force tough-cookie to 4.1.3 due to vulnerability ([#5092](https://github.com/Unleash/unleash/issues/5092))
- Remove ts-ignore and adapt tests ([#5103](https://github.com/Unleash/unleash/issues/5103))
- Remove invite link flag ([#5119](https://github.com/Unleash/unleash/issues/5119))
- Disable fsync in gh action postgres to speed up the tests ([#5139](https://github.com/Unleash/unleash/issues/5139))
- Add CHANGE_REQUEST_SCHEDULED to event types. ([#5162](https://github.com/Unleash/unleash/issues/5162))
### Refactor
- Expicit names in queries ([#4850](https://github.com/Unleash/unleash/issues/4850))
- Prefer eventService.storeEvent methods ([#4830](https://github.com/Unleash/unleash/issues/4830))
- Bubble promise instead of return await ([#4906](https://github.com/Unleash/unleash/issues/4906))
- Custom render should provide container ([#4938](https://github.com/Unleash/unleash/issues/4938))
- Make uiFlags typesafe ([#4996](https://github.com/Unleash/unleash/issues/4996))
- Feature toggle list query ([#5022](https://github.com/Unleash/unleash/issues/5022))
- Add test coverage ([#5046](https://github.com/Unleash/unleash/issues/5046))
- Create builder class for converting rows to avoid duplication ([#5050](https://github.com/Unleash/unleash/issues/5050))
- Add tests for /api/client/features ([#5057](https://github.com/Unleash/unleash/issues/5057))
- Move message banner interface to common file ([#5076](https://github.com/Unleash/unleash/issues/5076))
- Rename message banners to banners ([#5098](https://github.com/Unleash/unleash/issues/5098))
- Rename message banners to banners - events ([#5100](https://github.com/Unleash/unleash/issues/5100))
- Move version service scheduling to scheduler ([#5120](https://github.com/Unleash/unleash/issues/5120))
- Proxy service scheduler ([#5125](https://github.com/Unleash/unleash/issues/5125))
- Move metrics service scheduling ([#5129](https://github.com/Unleash/unleash/issues/5129))
- Slight clean up after GAing multiple roles ([#5133](https://github.com/Unleash/unleash/issues/5133))
- Type query params ([#5153](https://github.com/Unleash/unleash/issues/5153))
- Optimize queries ([#5158](https://github.com/Unleash/unleash/issues/5158))
### Testing
- Makes overview spec less flaky by doing 2 step search ([#4862](https://github.com/Unleash/unleash/issues/4862))
- Playground with dependencies ([#4936](https://github.com/Unleash/unleash/issues/4936))
- Added tests for has strategies and enabled strategies ([#5112](https://github.com/Unleash/unleash/issues/5112))
- Silent migration test ([#5131](https://github.com/Unleash/unleash/issues/5131))
- Speed up the tests ([#5140](https://github.com/Unleash/unleash/issues/5140))
### Bug
- Fix broken links from lychee ([#5127](https://github.com/Unleash/unleash/issues/5127))
- Remove strategies from copy breadcrumbs ([#5137](https://github.com/Unleash/unleash/issues/5137))
### Meta
- Add note to generate openapi docs before starting local dev ([#4976](https://github.com/Unleash/unleash/issues/4976))
## [5.5.7] - 2023-10-20
### Miscellaneous Tasks
- Add splash screen for oss segments (#5053) (#5097)
## [5.5.6] - 2023-10-09
### Bug Fixes
- Only delete SSO-synced group membership where membership was added by SSO sync (#4929)
## [5.5.5] - 2023-10-04
### Bug Fixes
- ReportTable not updating status dynamically (#4923)
## [5.5.4] - 2023-10-04
### Bug Fixes
- Update potentially-stale status dynamically (#4905) (#4920)
### Miscellaneous Tasks
- Automate changelog generation on release branch (#4914)
## [5.5.3] - 2023-09-28
### Bug Fixes
@ -3328,9 +3839,7 @@ All notable changes to this project will be documented in this file.
- Update dependency unleash-frontend to v4.2.12
- Disable projects (#1085)
# Changelog
# 4.2.0
## 4.2.0
- ix: add default sort order for built in envs (#1076)
- chore: mute expected test errors
@ -3489,13 +3998,13 @@ All notable changes to this project will be documented in this file.
- fix: convert files to typescript
- fix: convert feature-schema.test.js to typescript
# 4.1.4
## 4.1.4
- feat: Move environments to enterprise (#935)
- fix: correct failing feature toggle test
- fix: Cleanup new features API with env support (#929)
# 4.1.3
## 4.1.3
- fix: Added indices and primary key to feature_tag (#936)
- fix: failing test
@ -3504,7 +4013,7 @@ All notable changes to this project will be documented in this file.
- docs: add react-sdk to proxy docs.
- Update README.md
# 4.1.2
## 4.1.2
- chore: update frontend
- fix: fine tune db-config based on experience
@ -3516,32 +4025,32 @@ All notable changes to this project will be documented in this file.
- Fix/sso docs (#931)
- chore(deps): bump tar from 6.1.7 to 6.1.11 (#930)
# 4.1.1
## 4.1.1
- chore: update frontend
- fix: set correct projects count in metrics
# 4.1.0
## 4.1.0
- docs: Added mikefrancis/laravel-unleash (#927)
# 4.1.0-beta.15
## 4.1.0-beta.15
- chore: update frontend
- fix: make sure exising projects get :global: env automatically
- docs: cleanup unleash-hosted refereces
# 4.1.0-beta.14
## 4.1.0-beta.14
- fix: upgrade unleash-frontend to v4.1.0-beta.10
- fix: correct data format for FEATURE_CREATED event
# 4.1.0-beta.13
## 4.1.0-beta.13
- chore: update frontend
# 4.1.0-beta.12
## 4.1.0-beta.12
- chore: update frontend
- fix: oas docs on root
@ -3552,24 +4061,24 @@ All notable changes to this project will be documented in this file.
- fix: import schema needs to understand :global: env
- fix: import should not drop built-in strategies
# 4.1.0-beta.11
## 4.1.0-beta.11
- fix: bump unleash-frontend to 4.1.0-beta.7
- Update index.md
- Update feature-toggles-archive-api.md
- Update configuring-unleash.md
# 4.1.0-beta.10
## 4.1.0-beta.10
- chore: update yarn.lock
- Fix/feature events (#924)
- fix: getFeatureToggleAdmin should include project
# 4.1.0-beta.9
## 4.1.0-beta.9
- fix: upgrade unleash-frontend to version 4.1.0-beta.5
# 4.1.0-beta.8
## 4.1.0-beta.8
- chore: update unleash-frontend
- Update README.md
@ -3577,11 +4086,11 @@ All notable changes to this project will be documented in this file.
- Fix/switch project endpoint (#923)
- fix: only update name if not undefined
# 4.1.0-beta.7
## 4.1.0-beta.7
- feat: sync fields when logging in via SSO (#916)
# 4.1.0-beta.6
## 4.1.0-beta.6
- fix: bump unleash-frontend to 4.1.0-beta.3,
- fix: add php syntax highlighting to docs (#921)
@ -3594,23 +4103,23 @@ All notable changes to this project will be documented in this file.
- Fix Common Grammar Error in ReadMe (#914)
- WIP: Feat/quickstart oss (#912)
# 4.1.0-beta.5
## 4.1.0-beta.5
- fix: adjust logo in emails
- Revert "fix: uri encode smtp connection string (#901)"
-
# 4.1.0-beta.4
## 4.1.0-beta.4
- fix: Clean up exported types even more
# 4.1.0-beta.3
## 4.1.0-beta.3
- fix: exported types x2
# 4.1.0-beta.2
## 4.1.0-beta.2
- fix: export types from main entry
# 4.1.0-beta.1
## 4.1.0-beta.1
- fix: upgrade unleash-fronendt to 4.1.0.beta.2
- docs: Update Unleash Proxy docker pull instructions (#911)
- feat: Adds sendEmail flag to body of create user request (#894)
@ -3640,7 +4149,7 @@ All notable changes to this project will be documented in this file.
- Use absolute url to api-token doc
# 4.1.0-beta.0
## 4.1.0-beta.0
- fix: Use 4.0.9 of frontend
- Fix typo (#899)
@ -3654,7 +4163,7 @@ All notable changes to this project will be documented in this file.
- fix: return empty array if no features are found for project
- doc: Add rikudou/unleash-sdk to community clients (#885)
# 4.0.6-beta.1
## 4.0.6-beta.1
- feat: Wip/environments (#880)
- Fixed typo (#884)
@ -3676,28 +4185,28 @@ All notable changes to this project will be documented in this file.
- chore(deps): bump ws from 6.2.1 to 6.2.2 in /websitev2 (#869)
- doc: redirects for external links
# 4.0.4
## 4.0.4
- fix: userFeedback should not be allowed to throw
- fix: make sure routes/user handles api calls
# 4.0.3
## 4.0.3
- feat: pnps feedback (#862)
- fix: upgrade unleash-frontend to v4.0.4
- chore: docs updates
# 4.0.2
## 4.0.2
- fix: upgrade unleash-frontend to version 4.0.1
- fix: projects needs at least one owner
# 4.0.1
## 4.0.1
- fix: create config should allow all options params
- fix: a lot of minor docs improvements
# 4.0.0
## 4.0.0
- fix: upgrade unleash-frontend to version 4.0.0
- fix: add migration (#847)
@ -3705,43 +4214,43 @@ All notable changes to this project will be documented in this file.
- chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /website (#843)
- Add explanation of how to run multiple instances of Unleash to the Getting Started doc (#845
# 4.0.0-beta.6
## 4.0.0-beta.6
- fix: Upgrade unleash-frontend to version 4.0.0-beta.5
- fix: Update docs to prepare for version 4
# 4.0.0-beta.5
## 4.0.0-beta.5
- fix: upgrade to unleash-frontend 4.0.0-beta.4
- fix: versionInfo as part of ui-config
- fix: misunderstanding node URL api
- fix: demo auth type should support api token
# 4.0.0-beta.4
## 4.0.0-beta.4
- upgrade unleash-frontend to version 4.0.0-beta.3
- fix: convert to typescript
- fix: report email as not sent to fe if it throws (#844)
# 4.0.0-beta.3
## 4.0.0-beta.3
- chore: update changelog
- fix: reset-token-service should use unleashUrl
- chore: expose an endpoint to really delete a toggle (#808)
- fix: upgrade unleash-frontend to version 4.0.0-beta.2
# 4.0.0-beta.1
## 4.0.0-beta.1
- fix: upgrade unleash-frontend to version 4.0.0-beta.0
- fix: rbac now checks permission for both projects (#838)
- fix: an hour is 3600000 seconds not 60000 seconds
- fix: readd support for DATABASE_URL_FILE
# 4.0.0-beta.0
## 4.0.0-beta.0
- fix: reload of admin/api page yields 404
# 4.0.0-alpha.8
## 4.0.0-alpha.8
- feat: global events requires admin role
- fix: remove toast info from bootstrap controller (#834)
@ -3754,7 +4263,7 @@ All notable changes to this project will be documented in this file.
- fix: regular users are not API users
- Feat: format base path (#828)
# 4.0.0-alpha.7
## 4.0.0-alpha.7
- fix: more types
- fix: move permission to types
@ -3765,7 +4274,7 @@ All notable changes to this project will be documented in this file.
- feat: automatically add all existing users as owners to all existing … (#818)
- fix: project store was wrongly typing its id field as number (#822)
# 4.0.0-alpha.6
## 4.0.0-alpha.6
- feat: Teams addon for messaging on Microsoft teams (#814)
- feat: add user create/update/delete events (#807)
@ -3789,11 +4298,11 @@ All notable changes to this project will be documented in this file.
- fix: change default admin password
- fix: add types for node-fetch
# 4.0.0-alpha.5
## 4.0.0-alpha.5
- chore: update frontend
# 4.0.0-alpha.4
## 4.0.0-alpha.4
- feat: add option for LOG_LEVEL (#803)
- fix: make users emails case-insensitive (#804)
@ -3802,7 +4311,7 @@ All notable changes to this project will be documented in this file.
- fix: simplify isConfigured check
- fix: loading of emailtemplates
# 4.0.0-alpha.3
## 4.0.0-alpha.3
- fix: should allow revive toggles
- fix: hasPermission should not throw

View File

@ -53,8 +53,6 @@ const UsersList = () => {
const [delUser, setDelUser] = useState<IUser>();
const { planUsers, isBillingUsers } = useUsersPlan(users);
const accessOverviewEnabled = useUiFlag('accessOverview');
const [searchValue, setSearchValue] = useState('');
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
@ -271,26 +269,15 @@ const UsersList = () => {
onChange={setSearchValue}
/>
<PageHeader.Divider />
<ConditionallyRender
condition={
isEnterprise() &&
Boolean(accessOverviewEnabled)
}
show={() => (
<>
<Tooltip
title='Exports user access information'
arrow
describeChild
>
<IconButton onClick={downloadCSV}>
<Download />
</IconButton>
</Tooltip>
</>
)}
/>
<Tooltip
title='Exports user access information'
arrow
describeChild
>
<IconButton onClick={downloadCSV}>
<Download />
</IconButton>
</Tooltip>
<Button
variant='contained'
color='primary'

View File

@ -63,11 +63,11 @@ export const ConstraintAccordionEditBody: React.FC<IConstraintAccordionBody> =
<StyledLeftButton
type='button'
onClick={onSubmit}
variant='contained'
variant='outlined'
color='primary'
data-testid='CONSTRAINT_SAVE_BUTTON'
>
Save
Done
</StyledLeftButton>
<StyledRightButton
onClick={() => {

View File

@ -184,8 +184,8 @@ const ProjectForm: React.FC<IProjectForm> = ({
}
/>
<ConditionallyRender
condition={mode === 'Edit' && Boolean(setFeatureLimit)}
show={
condition={mode === 'Edit'}
show={() => (
<>
<Box
sx={{
@ -202,17 +202,15 @@ const ProjectForm: React.FC<IProjectForm> = ({
Leave it empty if you dont want to add a limit
</StyledSubtitle>
<StyledInputContainer>
{featureLimit && setFeatureLimit && (
<StyledInput
label={'Limit'}
name='value'
type={'number'}
value={featureLimit}
onChange={(e) =>
setFeatureLimit(e.target.value)
}
/>
)}
<StyledInput
label={'Limit'}
name='value'
type={'number'}
value={featureLimit!}
onChange={(e) =>
setFeatureLimit!(e.target.value)
}
/>
<ConditionallyRender
condition={
featureCount !== undefined &&
@ -226,7 +224,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
/>
</StyledInputContainer>
</>
}
)}
/>
<ConditionallyRender
condition={mode === 'Create' && isEnterprise()}

View File

@ -82,7 +82,7 @@ const useProjectForm = (
const getFeatureLimitAsNumber = () => {
if (featureLimit === '') {
return undefined;
return null;
}
return Number(featureLimit);
};

View File

@ -1,7 +1,10 @@
import React, { useContext } from 'react';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { CREATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
import {
CREATE_SEGMENT,
UPDATE_PROJECT_SEGMENT,
} from 'component/providers/AccessProvider/permissions';
import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
@ -112,7 +115,8 @@ export const CreateSegment = ({ modal }: ICreateSegmentProps) => {
>
<CreateButton
name='segment'
permission={CREATE_SEGMENT}
permission={[CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]}
projectId={projectId}
disabled={!hasValidConstraints || overSegmentValuesLimit}
data-testid={SEGMENT_CREATE_BTN_ID}
/>

View File

@ -1,5 +1,8 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { UPDATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
import {
UPDATE_PROJECT_SEGMENT,
UPDATE_SEGMENT,
} from 'component/providers/AccessProvider/permissions';
import { useSegmentsApi } from 'hooks/api/actions/useSegmentsApi/useSegmentsApi';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import { useSegment } from 'hooks/api/getters/useSegment/useSegment';
@ -135,7 +138,8 @@ export const EditSegment = ({ modal }: IEditSegmentProps) => {
mode='edit'
>
<UpdateButton
permission={UPDATE_SEGMENT}
permission={[UPDATE_SEGMENT, UPDATE_PROJECT_SEGMENT]}
projectId={projectId}
disabled={!hasValidConstraints || overSegmentValuesLimit}
data-testid={SEGMENT_SAVE_BTN_ID}
>

View File

@ -1,9 +1,13 @@
import { styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
import { CREATE_SEGMENT } from 'component/providers/AccessProvider/permissions';
import {
CREATE_SEGMENT,
UPDATE_PROJECT_SEGMENT,
} from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext';
import { useContext } from 'react';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
const StyledDiv = styled('div')(({ theme }) => ({
display: 'flex',
@ -35,6 +39,7 @@ const StyledLink = styled(Link)(({ theme }) => ({
}));
export const SegmentEmpty = () => {
const projectId = useOptionalPathParam('projectId');
const { hasAccess } = useContext(AccessContext);
return (
@ -46,7 +51,10 @@ export const SegmentEmpty = () => {
and can be reused.
</StyledParagraph>
<ConditionallyRender
condition={hasAccess(CREATE_SEGMENT)}
condition={hasAccess(
[CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT],
projectId,
)}
show={
<StyledLink to='/segments/create'>
Create your first segment

View File

@ -72,6 +72,7 @@ export const SegmentForm: React.FC<ISegmentProps> = ({
condition={currentStep === 2}
show={
<SegmentFormStepTwo
project={project}
constraints={constraints}
setConstraints={setConstraints}
setCurrentStep={setCurrentStep}

View File

@ -8,6 +8,7 @@ import { CreateUnleashContext } from 'component/context/CreateUnleashContext/Cre
import {
CREATE_CONTEXT_FIELD,
CREATE_SEGMENT,
UPDATE_PROJECT_SEGMENT,
UPDATE_SEGMENT,
} from 'component/providers/AccessProvider/permissions';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
@ -32,6 +33,7 @@ import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentL
import { GO_BACK } from 'constants/navigate';
interface ISegmentFormPartTwoProps {
project?: string;
constraints: IConstraint[];
setConstraints: React.Dispatch<React.SetStateAction<IConstraint[]>>;
setCurrentStep: React.Dispatch<React.SetStateAction<SegmentFormStep>>;
@ -101,6 +103,7 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
children,
project,
constraints,
setConstraints,
setCurrentStep,
@ -112,7 +115,10 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
const { context = [] } = useUnleashContext();
const [open, setOpen] = useState(false);
const segmentValuesCount = useSegmentValuesCount(constraints);
const modePermission = mode === 'create' ? CREATE_SEGMENT : UPDATE_SEGMENT;
const modePermission =
mode === 'create'
? [CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]
: [UPDATE_SEGMENT, UPDATE_PROJECT_SEGMENT];
const { segmentValuesLimit } = useSegmentLimits();
const overSegmentValuesLimit: boolean = Boolean(
@ -197,7 +203,7 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
ref={constraintsAccordionListRef}
constraints={constraints}
setConstraints={
hasAccess(modePermission)
hasAccess(modePermission, project)
? setConstraints
: undefined
}

View File

@ -24,6 +24,13 @@ type ApiErrorHandler = (
requestId: string,
) => void;
type ApiCaller = () => Promise<Response>;
type RequestFunction = (
apiCaller: ApiCaller,
requestId: string,
loadingOn?: boolean,
) => Promise<Response>;
interface IUseAPI {
handleBadRequest?: ApiErrorHandler;
handleNotFound?: ApiErrorHandler;
@ -33,6 +40,29 @@ interface IUseAPI {
propagateErrors?: boolean;
}
const timeApiCallStart = (requestId: string) => {
// Store the start time in milliseconds
console.log(`Starting timing for request: ${requestId}`);
return Date.now();
};
const timeApiCallEnd = (startTime: number, requestId: string) => {
// Calculate the end time and subtract the start time
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`Timing for request ${requestId}: ${duration} ms`);
if (duration > 500) {
console.error(
'API call took over 500ms. This may indicate a rendering performance problem in your React component.',
requestId,
duration,
);
}
return duration;
};
const useAPI = ({
handleBadRequest,
handleNotFound,
@ -157,6 +187,27 @@ const useAPI = ({
],
);
const requestWithTimer = (requestFunction: RequestFunction) => {
return async (
apiCaller: () => Promise<Response>,
requestId: string,
loadingOn: boolean = true,
) => {
const start = timeApiCallStart(
requestId || `Unknown request happening on ${apiCaller}`,
);
const res = await requestFunction(apiCaller, requestId, loadingOn);
timeApiCallEnd(
start,
requestId || `Unknown request happening on ${apiCaller}`,
);
return res;
};
};
const makeRequest = useCallback(
async (
apiCaller: () => Promise<Response>,
@ -187,6 +238,27 @@ const useAPI = ({
[handleResponses],
);
const makeLightRequest = useCallback(
async (
apiCaller: () => Promise<Response>,
requestId: string,
loadingOn: boolean = true,
): Promise<Response> => {
try {
const res = await apiCaller();
if (!res.ok) {
throw new Error();
}
return res;
} catch (e) {
throw new Error('Could not make request | makeLightRequest');
}
},
[],
);
const createRequest = useCallback(
(path: string, options: any, requestId: string = '') => {
const defaultOptions: RequestInit = {
@ -207,9 +279,17 @@ const useAPI = ({
[],
);
const makeRequestWithTimer = requestWithTimer(makeRequest);
const makeLightRequestWithTimer = requestWithTimer(makeLightRequest);
const isDevelopment = process.env.NODE_ENV === 'development';
return {
loading,
makeRequest,
makeRequest: isDevelopment ? makeRequestWithTimer : makeRequest,
makeLightRequest: isDevelopment
? makeLightRequestWithTimer
: makeLightRequest,
createRequest,
errors,
};

View File

@ -65,7 +65,6 @@ export type UiFlags = {
doraMetrics?: boolean;
variantTypeNumber?: boolean;
privateProjects?: boolean;
accessOverview?: boolean;
dependentFeatures?: boolean;
banners?: boolean;
disableEnvsOnRevive?: boolean;

View File

@ -1,8 +1,13 @@
{
"name": "unleash-server",
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
"version": "5.6.0-main",
"keywords": ["unleash", "feature toggle", "feature", "toggle"],
"version": "5.6.8",
"keywords": [
"unleash",
"feature toggle",
"feature",
"toggle"
],
"files": [
"dist",
"docs",
@ -75,11 +80,23 @@
"testTimeout": 10000,
"globalSetup": "./scripts/jest-setup.js",
"transform": {
"^.+\\.tsx?$": ["@swc/jest"]
"^.+\\.tsx?$": [
"@swc/jest"
]
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": ["/dist/", "/node_modules/", "/frontend/"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"],
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/",
"/frontend/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/dist/",
@ -122,7 +139,6 @@
"knex": "^2.5.1",
"lodash.get": "^4.4.2",
"lodash.groupby": "^4.6.0",
"lodash.isequal": "^4.5.0",
"lodash.sortby": "^4.7.0",
"log4js": "^6.0.0",
"make-fetch-happen": "^11.0.0",
@ -216,7 +232,11 @@
"tough-cookie": "4.1.3"
},
"lint-staged": {
"*.{js,ts}": ["biome check --apply"],
"*.json": ["biome format --write --no-errors-on-unmatched"]
"*.{js,ts}": [
"biome check --apply"
],
"*.json": [
"biome format --write --no-errors-on-unmatched"
]
}
}

View File

@ -71,7 +71,6 @@ exports[`should create default config 1`] = `
},
"flagResolver": FlagResolver {
"experiments": {
"accessOverview": false,
"anonymiseEventLog": false,
"banners": false,
"caseInsensitiveInOperators": false,

View File

@ -468,11 +468,14 @@ export class AccessStore implements IAccessStore {
roleId: number,
projectId?: string,
): Promise<void> {
return this.db(T.ROLE_USER).insert({
user_id: userId,
role_id: roleId,
project: projectId,
});
await this.db(T.ROLE_USER)
.insert({
user_id: userId,
role_id: roleId,
project: projectId,
})
.onConflict(['user_id', 'role_id', 'project'])
.ignore();
}
async removeUserFromRole(

View File

@ -244,8 +244,9 @@ class ProjectStore implements IProjectStore {
const settingsRow = await this.db(SETTINGS_TABLE)
.insert({
project: project.id,
project_mode: project.mode,
default_stickiness: project.defaultStickiness,
feature_limit: project.featureLimit,
project_mode: project.mode,
})
.returning('*');
return this.mapRow({ ...row[0], ...settingsRow[0] });
@ -265,6 +266,7 @@ class ProjectStore implements IProjectStore {
await this.db(TABLE)
.where({ id: data.id })
.update(this.fieldToRow(data));
if (
data.defaultStickiness !== undefined ||
data.featureLimit !== undefined

View File

@ -15,7 +15,7 @@ class PermissionError extends UnleashError {
const permissionsMessage =
permissions.length === 1
? `the "${permissions[0]}" permission`
: `all of the following permissions: ${permissions
: `one of the following permissions: ${permissions
.map((perm) => `"${perm}"`)
.join(', ')}`;

View File

@ -165,6 +165,26 @@ test('should search matching features by tag', async () => {
});
});
test('should return all feature tags', async () => {
await app.createFeature('my_feature_a');
await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' });
await app.addTag('my_feature_a', { type: 'simple', value: 'second_tag' });
const { body } = await searchFeatures({});
expect(body).toMatchObject({
features: [
{
name: 'my_feature_a',
tags: [
{ type: 'simple', value: 'my_tag' },
{ type: 'simple', value: 'second_tag' },
],
},
],
});
});
test('should return empty features', async () => {
const { body } = await searchFeatures({ query: '' });
expect(body).toMatchObject({ features: [] });

View File

@ -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;
}

View File

@ -102,8 +102,6 @@ import { IPrivateProjectChecker } from '../private-project/privateProjectChecker
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
import EventService from '../../services/event-service';
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
import isEqual from 'lodash.isequal';
import { deepDiff } from './deep-diff';
interface IFeatureContext {
featureName: string;
@ -1058,22 +1056,6 @@ class FeatureToggleService {
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')
? featuresFromFeatureToggleStore
: 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')
? featuresFromFeatureToggleStore
: featuresFromClientStore;

View File

@ -105,18 +105,6 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable {
};
}
const getUniqueRows = (rows: any[]) => {
const seen = {};
return rows.filter((row) => {
const key = `${row.environment}-${row.feature_name}`;
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
});
};
const sortEnvironments = (overview: IFeatureOverview) => {
return Object.values(overview).map((data: IFeatureOverview) => ({
...data,
@ -252,7 +240,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
environment: string,
): Promise<void> {
await this.db('feature_strategies')
.where({ feature_name: featureName, environment })
.where({
feature_name: featureName,
environment,
})
.del();
}
@ -295,8 +286,14 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
environment,
})
.orderBy([
{ column: 'sort_order', order: 'asc' },
{ column: 'created_at', order: 'asc' },
{
column: 'sort_order',
order: 'asc',
},
{
column: 'created_at',
order: 'asc',
},
]);
stopTimer();
return rows.map(mapRow);
@ -342,11 +339,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
query.leftJoin(
'last_seen_at_metrics',
'last_seen_at_metrics.environment',
'features_view.environment_name',
);
query.leftJoin('last_seen_at_metrics', function () {
this.on(
'last_seen_at_metrics.environment',
'=',
'features_view.environment_name',
).andOn(
'last_seen_at_metrics.feature_name',
'=',
'features_view.name',
);
});
// Override feature view for now
selectColumns.push(
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
@ -657,7 +660,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
query = query.select(selectColumns);
const rows = await query;
if (rows.length > 0) {
const overview = this.getFeatureOverviewData(getUniqueRows(rows));
const overview = this.getFeatureOverviewData(rows);
return sortEnvironments(overview);
}
return [];
@ -772,7 +775,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
query = query.select(selectColumns);
const rows = await query;
if (rows.length > 0) {
const overview = this.getFeatureOverviewData(getUniqueRows(rows));
const overview = this.getFeatureOverviewData(rows);
return sortEnvironments(overview);
}
return [];
@ -781,9 +784,18 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
getFeatureOverviewData(rows): IFeatureOverview {
return rows.reduce((acc, row) => {
if (acc[row.feature_name] !== undefined) {
acc[row.feature_name].environments.push(
FeatureStrategiesStore.getEnvironment(row),
const environmentExists = acc[
row.feature_name
].environments.some(
(existingEnvironment) =>
existingEnvironment.name === row.environment,
);
if (!environmentExists) {
acc[row.feature_name].environments.push(
FeatureStrategiesStore.getEnvironment(row),
);
}
if (this.isNewTag(acc[row.feature_name], row)) {
this.addTag(acc[row.feature_name], row);
}
@ -799,6 +811,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
impressionData: row.impression_data,
environments: [FeatureStrategiesStore.getEnvironment(row)],
};
if (this.isNewTag(acc[row.feature_name], row)) {
this.addTag(acc[row.feature_name], row);
}
@ -858,7 +871,10 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
environment: String,
): Promise<void> {
await this.db(T.featureStrategies)
.where({ project_name: projectId, environment })
.where({
project_name: projectId,
environment,
})
.del();
}

View File

@ -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,
},
]);
});
});

View File

@ -154,3 +154,53 @@ test('response should include last seen at per environment for multiple environm
expect(production.name).toBe('production');
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',
},
]);
});

View File

@ -40,9 +40,9 @@ class PrivateProjectStore implements IPrivateProjectStore {
'roles.type': 'root',
})
.count('*')
.first();
.then((res) => Number(res[0].count));
if (!isViewer || isViewer.count === 0) {
if (isViewer === 0) {
return ALL_PROJECT_ACCESS;
}

View File

@ -30,6 +30,7 @@ import {
IFlagResolver,
NONE,
UPDATE_FEATURE_STRATEGY,
UPDATE_PROJECT_SEGMENT,
UPDATE_SEGMENT,
serializeDates,
} from '../../types';
@ -165,7 +166,7 @@ export class SegmentsController extends Controller {
method: 'delete',
path: '/:id',
handler: this.removeSegment,
permission: DELETE_SEGMENT,
permission: [DELETE_SEGMENT, UPDATE_PROJECT_SEGMENT],
acceptAnyContentType: true,
middleware: [
openApiService.validPath({
@ -186,7 +187,7 @@ export class SegmentsController extends Controller {
method: 'put',
path: '/:id',
handler: this.updateSegment,
permission: UPDATE_SEGMENT,
permission: [UPDATE_SEGMENT, UPDATE_PROJECT_SEGMENT],
middleware: [
openApiService.validPath({
summary: 'Update segment by id',
@ -225,7 +226,7 @@ export class SegmentsController extends Controller {
method: 'post',
path: '',
handler: this.createSegment,
permission: CREATE_SEGMENT,
permission: [CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT],
middleware: [
openApiService.validPath({
summary: 'Create a new segment',
@ -348,7 +349,10 @@ export class SegmentsController extends Controller {
): Promise<void> {
const { id } = req.params;
const { user } = req;
const strategies = await this.segmentService.getStrategies(id, user.id);
const strategies = await this.segmentService.getVisibleStrategies(
id,
user.id,
);
// Remove unnecessary IFeatureStrategy fields from the response.
const segmentStrategies = strategies.map((strategy) => ({
@ -369,8 +373,7 @@ export class SegmentsController extends Controller {
res: Response,
): Promise<void> {
const id = Number(req.params.id);
const { user } = req;
const strategies = await this.segmentService.getStrategies(id, user.id);
const strategies = await this.segmentService.getAllStrategies(id);
if (strategies.length > 0) {
res.status(409).send();

View File

@ -7,12 +7,16 @@ import ApiUser from '../types/api-user';
import { IFeatureToggleStore } from '../features/feature-toggle/types/feature-toggle-store-type';
import FakeFeatureToggleStore from '../features/feature-toggle/fakes/fake-feature-toggle-store';
import { ApiTokenType } from '../types/models/api-token';
import { ISegmentStore } from '../types';
import FakeSegmentStore from '../../test/fixtures/fake-segment-store';
let config: IUnleashConfig;
let featureToggleStore: IFeatureToggleStore;
let segmentStore: ISegmentStore;
beforeEach(() => {
featureToggleStore = new FakeFeatureToggleStore();
segmentStore = new FakeSegmentStore();
config = createTestConfig();
});
@ -21,7 +25,11 @@ test('should add checkRbac to request', () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
@ -40,7 +48,11 @@ test('should give api-user ADMIN permission', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -66,7 +78,11 @@ test('should not give api-user ADMIN permission', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -94,7 +110,11 @@ test('should not allow user to miss userId', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -116,7 +136,11 @@ test('should return false for missing user', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {};
@ -134,7 +158,11 @@ test('should verify permission for root resource', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -163,7 +191,11 @@ test('should lookup projectId from params', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -198,7 +230,11 @@ test('should lookup projectId from feature toggle', async () => {
featureToggleStore.getProjectId = jest.fn().mockReturnValue(projectId);
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -231,7 +267,11 @@ test('should lookup projectId from data', async () => {
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
@ -266,7 +306,11 @@ test('Does not double check permission if not changing project when updating tog
};
featureToggleStore.getProjectId = jest.fn().mockReturnValue(oldProjectId);
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
user: new User({ username: 'user', id: 1 }),
@ -290,7 +334,11 @@ test('UPDATE_TAG_TYPE does not need projectId', async () => {
hasPermission: jest.fn().mockReturnValue(true),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
user: new User({ username: 'user', id: 1 }),
@ -314,7 +362,11 @@ test('DELETE_TAG_TYPE does not need projectId', async () => {
hasPermission: jest.fn().mockReturnValue(true),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {
user: new User({ username: 'user', id: 1 }),
@ -340,7 +392,11 @@ test('should not expect featureName for UPDATE_FEATURE when projectId specified'
hasPermission: jest.fn(),
};
const func = rbacMiddleware(config, { featureToggleStore }, accessService);
const func = rbacMiddleware(
config,
{ featureToggleStore, segmentStore },
accessService,
);
const cb = jest.fn();
const req: any = {

View File

@ -3,6 +3,7 @@ import {
DELETE_FEATURE,
ADMIN,
UPDATE_FEATURE,
UPDATE_PROJECT_SEGMENT,
} from '../types/permissions';
import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
@ -32,7 +33,10 @@ export function findParam(
const rbacMiddleware = (
config: Pick<IUnleashConfig, 'getLogger'>,
{ featureToggleStore }: Pick<IUnleashStores, 'featureToggleStore'>,
{
featureToggleStore,
segmentStore,
}: Pick<IUnleashStores, 'featureToggleStore' | 'segmentStore'>,
accessService: PermissionChecker,
): any => {
const logger = config.getLogger('/middleware/rbac-middleware.ts');
@ -87,6 +91,18 @@ const rbacMiddleware = (
projectId = 'default';
}
// DELETE segment does not include information about the segment's project
// This is needed to check if the user has the right permissions on a project level
if (
!projectId &&
permissionsArray.includes(UPDATE_PROJECT_SEGMENT) &&
params.id
) {
const { id } = params;
const { project } = await segmentStore.get(id);
projectId = project;
}
return accessService.hasPermission(
user,
permissionsArray,

View File

@ -13,7 +13,17 @@ export interface ISegmentService {
get(id: number): Promise<ISegment>;
getStrategies(id: number, userId: number): Promise<IFeatureStrategy[]>;
/**
* Gets all strategies for a segment
* This is NOT considering the private projects
* For most use cases, use `getVisibleStrategies`
*/
getAllStrategies(id: number): Promise<IFeatureStrategy[]>;
getVisibleStrategies(
id: number,
userId: number,
): Promise<IFeatureStrategy[]>;
validateName(name: string): Promise<void>;

View File

@ -347,9 +347,9 @@ export class AccessService {
DEFAULT_PROJECT,
);
} catch (error) {
throw new Error(
`Could not add role=${newRootRole.name} to userId=${userId}`,
);
const message = `Could not add role=${newRootRole.name} to userId=${userId}`;
this.logger.error(message, error);
throw new Error(message);
}
} else {
throw new BadDataError(`Could not find rootRole=${role}`);

View File

@ -14,6 +14,26 @@ export interface FeaturesTable {
environment: string;
}
const prepareLastSeenInput = (data: LastSeenInput[]) => {
const now = new Date();
const sortedData = data.sort(
(a, b) =>
a.featureName.localeCompare(b.featureName) ||
a.environment.localeCompare(b.environment),
);
const inserts = sortedData.map((item) => {
return {
feature_name: item.featureName,
environment: item.environment,
last_seen_at: now,
};
});
return inserts;
};
export default class LastSeenStore implements ILastSeenStore {
private db: Db;
@ -32,18 +52,10 @@ export default class LastSeenStore implements ILastSeenStore {
}
async setLastSeen(data: LastSeenInput[]): Promise<void> {
const now = new Date();
try {
const inserts = data.map((item) => {
return {
feature_name: item.featureName,
environment: item.environment,
last_seen_at: now,
};
});
const inserts = prepareLastSeenInput(data);
const batchSize = 1000;
const batchSize = 500;
for (let i = 0; i < inserts.length; i += batchSize) {
const batch = inserts.slice(i, i + batchSize);

View File

@ -77,18 +77,15 @@ export class SegmentService implements ISegmentService {
return this.segmentStore.getActiveForClient();
}
// Used by unleash-enterprise.
async getByStrategy(strategyId: string): Promise<ISegment[]> {
return this.segmentStore.getByStrategy(strategyId);
}
// Used by unleash-enterprise.
async getStrategies(
async getVisibleStrategies(
id: number,
userId: number,
): Promise<IFeatureStrategy[]> {
const strategies =
await this.featureStrategiesStore.getStrategiesBySegment(id);
const strategies = await this.getAllStrategies(id);
if (this.flagResolver.isEnabled('privateProjects')) {
const accessibleProjects =
await this.privateProjectChecker.getUserAccessibleProjects(
@ -105,6 +102,12 @@ export class SegmentService implements ISegmentService {
return strategies;
}
async getAllStrategies(id: number): Promise<IFeatureStrategy[]> {
const strategies =
await this.featureStrategiesStore.getStrategiesBySegment(id);
return strategies;
}
async create(
data: unknown,
user: Partial<Pick<User, 'username' | 'email'>>,

View File

@ -26,7 +26,6 @@ export type IFlagKey =
| 'featureNamingPattern'
| 'doraMetrics'
| 'variantTypeNumber'
| 'accessOverview'
| 'privateProjects'
| 'dependentFeatures'
| 'disableMetrics'
@ -137,10 +136,6 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
false,
),
accessOverview: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW,
false,
),
disableMetrics: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
false,

View File

@ -7,7 +7,7 @@ exports.up = function(db, cb) {
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
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;
@ -19,7 +19,7 @@ exports.up = function(db, cb) {
CREATE TRIGGER unleash_update_stat_environment_changes
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);
};

View File

@ -42,7 +42,6 @@ process.nextTick(async () => {
doraMetrics: true,
variantTypeNumber: true,
privateProjects: true,
accessOverview: true,
dependentFeatures: true,
useLastSeenRefactor: true,
disableEnvsOnRevive: true,

View File

@ -346,10 +346,15 @@ export const insertLastSeenAt = async (
environment: string = 'default',
date: string = '2023-10-01 12:34:56',
): 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}');`);
return date;
return date;
} catch (err) {
console.log(err);
return Promise.resolve('');
}
};
export const insertFeatureEnvironmentsLastSeen = async (