2 |
10 |
11 |
12 |
13 |
14 | {{ (selectedApp$ | async)?.name }}
15 |
16 |
17 |
21 |
24 |
25 |
26 |
27 |
28 | Feature Toggles
29 | Config Properties
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "project": {
3 | "version": "1.0.0-beta.19-3",
4 | "name": "remote-config-dashboard"
5 | },
6 | "apps": [{
7 | "root": "src",
8 | "outDir": "dist",
9 | "assets": [
10 | "assets",
11 | "favicon.ico"
12 | ],
13 | "index": "index.html",
14 | "main": "main.ts",
15 | "polyfills": "polyfills.ts",
16 | "test": "test.ts",
17 | "tsconfig": "tsconfig.json",
18 | "prefix": "app",
19 | "mobile": false,
20 | "styles": [
21 | "styles.scss",
22 | "../node_modules/clarity-icons/clarity-icons.min.css",
23 | "../node_modules/clarity-ui/clarity-ui.min.css",
24 | "../node_modules/jsoneditor/dist/jsoneditor.min.css"
25 | ],
26 | "scripts": [
27 | "../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
28 | "../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
29 | "../node_modules/clarity-icons/clarity-icons.min.js",
30 | "../node_modules/jsoneditor/dist/jsoneditor.js"
31 | ],
32 | "environmentSource": "environments/environment.ts",
33 | "environments": {
34 | "dev": "environments/environment.ts",
35 | "prod": "environments/environment.prod.ts"
36 | }
37 | }],
38 | "addons": [],
39 | "packages": [],
40 | "e2e": {
41 | "protractor": {
42 | "config": "./protractor.conf.js"
43 | }
44 | },
45 | "test": {
46 | "karma": {
47 | "config": "./karma.conf.js"
48 | }
49 | },
50 | "defaults": {
51 | "styleExt": "scss",
52 | "prefixInterfaces": false,
53 | "inline": {
54 | "style": false,
55 | "template": false
56 | },
57 | "spec": {
58 | "class": false,
59 | "component": true,
60 | "directive": true,
61 | "module": false,
62 | "pipe": true,
63 | "service": true
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { AppEffects } from './state-management/app';
2 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3 | import { app } from './state-management/app';
4 | import { apps } from './state-management/apps';
5 | import { properties } from './state-management/properties';
6 | import { featureToggles } from './state-management/feature-toggles';
7 | import { StoreDevtoolsModule } from '@ngrx/store-devtools';
8 | import { BrowserModule} from '@angular/platform-browser';
9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
10 | import { NgModule, } from '@angular/core';
11 | import { FormsModule } from '@angular/forms';
12 | import { HttpModule } from '@angular/http';
13 | import { ClarityModule } from 'clarity-angular';
14 | import { AppComponent } from './app.component';
15 | import { StoreModule } from '@ngrx/store';
16 | import { FeatureTogglesComponent } from './feature-toggles/feature-toggles.component';
17 | import { ReactiveFormsModule } from '@angular/forms';
18 | import { ConfigPropertiesComponent } from './config-properties/config-properties.component';
19 | import { EffectsModule } from '@ngrx/effects';
20 | import { AppsEffects } from './state-management/apps';
21 | import { FilterPipe } from './shared/pipes/filter.pipe';
22 | import { AddToggleModalComponent } from './add-toggle-modal/add-toggle-modal.component';
23 |
24 | @NgModule({
25 | declarations: [
26 | AppComponent,
27 | FeatureTogglesComponent,
28 | ConfigPropertiesComponent,
29 | FilterPipe,
30 | AddToggleModalComponent
31 | ],
32 | imports: [
33 | BrowserModule,
34 | FormsModule,
35 | HttpModule,
36 | ClarityModule.forChild(),
37 | StoreModule.provideStore({featureToggles, properties, apps, app}),
38 | StoreDevtoolsModule.instrumentOnlyWithExtension({
39 | maxAge: 5
40 | }),
41 | ReactiveFormsModule,
42 | BrowserAnimationsModule,
43 | EffectsModule.run(AppsEffects),
44 | EffectsModule.run(AppEffects)
45 | ],
46 | providers: [FilterPipe],
47 | bootstrap: [AppComponent],
48 | schemas: [
49 | CUSTOM_ELEMENTS_SCHEMA
50 | ],
51 | })
52 | export class AppModule { }
53 |
--------------------------------------------------------------------------------
/src/app/feature-toggles/feature-toggles.component.html:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 | Remove toggle
38 |
39 |
{{this.confirmationMessage}}
40 |
41 |
45 |
--------------------------------------------------------------------------------
/src/app/feature-toggles/feature-toggles.component.ts:
--------------------------------------------------------------------------------
1 | import { AddToggleModalComponent } from '../add-toggle-modal/add-toggle-modal.component';
2 | import { FeatureToggle } from '../state-management/feature-toggles';
3 | import { Store } from '@ngrx/store';
4 | import { Observable } from 'rxjs/Rx';
5 | import { Component, OnInit, ViewChild } from '@angular/core';
6 | import { ADD_FEATURE_TOGGLE, UPDATE_FEATURE_TOGGLE, REMOVE_FEATURE_TOGGLE } from '../state-management/feature-toggles';
7 |
8 | @Component({
9 | selector: 'app-feature-toggles',
10 | templateUrl: './feature-toggles.component.html',
11 | styleUrls: ['./feature-toggles.component.scss']
12 | })
13 | export class FeatureTogglesComponent implements OnInit {
14 |
15 | @ViewChild(AddToggleModalComponent) addToggleModal: AddToggleModalComponent
16 |
17 | featureToggles$: Observable
>;
18 | isConfirmationModalOpen = false;
19 | selectedToggleName: String;
20 | confirmationMessage: String;
21 |
22 | constructor(private store: Store) {
23 | this.featureToggles$ = this.store.select('featureToggles');
24 | }
25 |
26 | ngOnInit() {
27 | }
28 |
29 | ngAfterViewInit() {
30 | //pass array of toggles to add modal, to check if a toggle already exists before adding
31 | this.featureToggles$.subscribe((toggles) => {
32 | this.addToggleModal.featureToggles = toggles;
33 | });
34 | }
35 |
36 | openRemovalConfirmationModal(name: String) {
37 | this.selectedToggleName = name;
38 | this.confirmationMessage = `Are you sure you want to remove the toggle ${name}?`;
39 | this.isConfirmationModalOpen = true;
40 | }
41 |
42 | confirmToggleRemoval() {
43 | this.isConfirmationModalOpen = false;
44 | this.removeFeatureToggle(this.selectedToggleName)
45 | }
46 |
47 | updateFeatureToggle(name: String, state: boolean) {
48 | this.store.dispatch({
49 | type: UPDATE_FEATURE_TOGGLE,
50 | payload:
51 | {
52 | name: name,
53 | state: state
54 | }
55 | });
56 | }
57 |
58 | removeFeatureToggle(name: String) {
59 | this.store.dispatch({
60 | type: REMOVE_FEATURE_TOGGLE,
61 | payload:
62 | {
63 | name: name
64 | }
65 | });
66 | }
67 | }
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/app/add-toggle-modal/add-toggle-modal.component.html:
--------------------------------------------------------------------------------
1 |
2 | Create a new feature toggle
3 |
4 |
5 |
6 |
7 | A toggle with this name already exists.
8 |
9 |
10 |
11 |
35 |
36 |
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remote-config-dashboard",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "angular-cli": {},
6 | "scripts": {
7 | "start": "ng serve",
8 | "lint": "tslint \"src/**/*.ts\"",
9 | "test": "ng test",
10 | "pree2e": "webdriver-manager update",
11 | "e2e": "protractor"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "~4.0.1",
16 | "@angular/common": "~4.0.1",
17 | "@angular/compiler": "~4.0.1",
18 | "@angular/core": "~4.0.1",
19 | "@angular/forms": "~4.0.1",
20 | "@angular/http": "~4.0.1",
21 | "@angular/platform-browser": "~4.0.1",
22 | "@angular/platform-browser-dynamic": "~4.0.1",
23 | "@angular/router": "~4.0.1",
24 | "@ngrx/core": "^1.2.0",
25 | "@ngrx/effects": "^2.0.2",
26 | "@ngrx/store": "^2.2.1",
27 | "@ngrx/store-devtools": "^3.2.3",
28 | "@webcomponents/custom-elements": "^1.0.0-alpha.3",
29 | "ajv": "5.0.1-beta.3",
30 | "clarity-angular": "^0.9.3",
31 | "clarity-icons": "^0.9.3",
32 | "clarity-ui": "^0.9.3",
33 | "core-js": "^2.4.1",
34 | "jsoneditor": "^5.5.11",
35 | "karma-coverage-istanbul-reporter": "^1.2.0",
36 | "karma-jasmine-html-reporter": "^0.2.2",
37 | "mutationobserver-shim": "^0.3.2",
38 | "ngrx-domains": "^1.0.0-alpha.4",
39 | "rxjs": "5.1.0",
40 | "ts-helpers": "^1.1.1",
41 | "zone.js": "^0.8.4"
42 | },
43 | "devDependencies": {
44 | "@angular/cli": "1.0.0",
45 | "@angular/compiler-cli": "^4.0.1",
46 | "@types/jasmine": "^2.2.30",
47 | "@types/jsoneditor": "^5.5.2",
48 | "@types/node": "^6.0.60",
49 | "codelyzer": "1.0.0-beta.1",
50 | "jasmine-core": "2.4.1",
51 | "jasmine-spec-reporter": "2.5.0",
52 | "karma": "1.2.0",
53 | "karma-chrome-launcher": "^2.0.0",
54 | "karma-cli": "^1.0.1",
55 | "karma-jasmine": "^1.0.2",
56 | "karma-remap-istanbul": "^0.2.1",
57 | "karma-teamcity-reporter": "^1.0.0",
58 | "protractor": "4.0.9",
59 | "ts-node": "1.2.1",
60 | "tslint": "3.13.0",
61 | "typescript": "~2.2.0",
62 | "webdriver-manager": "10.2.5"
63 | }
64 | }
--------------------------------------------------------------------------------
/src/app/add-toggle-modal/add-toggle-modal.component.ts:
--------------------------------------------------------------------------------
1 | import { ViewChild } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import { ADD_FEATURE_TOGGLE, FeatureToggle, featureToggles } from '../state-management/feature-toggles';
4 | import { Component, OnInit } from '@angular/core';
5 | import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
6 | import { ChangeDetectorRef } from '@angular/core';
7 |
8 | @Component({
9 | selector: 'add-toggle-modal',
10 | templateUrl: './add-toggle-modal.component.html',
11 | styleUrls: ['./add-toggle-modal.component.scss']
12 | })
13 | export class AddToggleModalComponent implements OnInit {
14 | featureToggleForm: any;
15 | submitted = false;
16 | isAddToggleModalOpen = false;
17 | showToggleExistsWarning = false;
18 | featureToggles: FeatureToggle[];
19 |
20 | @ViewChild('toggleName') toggleNameInput;
21 |
22 | constructor(private fb: FormBuilder, private store: Store, private cdRef : ChangeDetectorRef) { }
23 |
24 | ngOnInit() {
25 | this.featureToggleForm = this.fb.group({
26 | toggleName: new FormControl('', Validators.required),
27 | toggleState: new FormGroup({
28 | state: new FormControl('')
29 | })
30 | });
31 | }
32 |
33 |
34 | openAddToggleModal() {
35 | this.featureToggleForm.reset();
36 | this.showToggleExistsWarning = false;
37 | this.submitted = false;
38 | this.isAddToggleModalOpen = true;
39 | setTimeout(() => {
40 | this.toggleNameInput.nativeElement.focus();
41 | }, 0);
42 |
43 | }
44 |
45 | addToggleClicked() {
46 | this.showToggleExistsWarning = false;
47 | let formData = this.featureToggleForm.value;
48 |
49 | if (this.doesToggleAlreadyExist(formData.toggleName)) {
50 | this.showToggleExistsWarning = true;
51 | } else {
52 | this.isAddToggleModalOpen = false;
53 | this.cdRef.detectChanges();
54 | this.addFeatureToggle({
55 | name: formData.toggleName,
56 | state: !!formData.toggleState.state
57 | })
58 | }
59 | }
60 | doesToggleAlreadyExist(name) {
61 | return this.featureToggles.filter((toggle) => toggle.name === name).length;
62 | }
63 |
64 | addFeatureToggle(toggle: FeatureToggle) {
65 | this.store.dispatch({
66 | type: ADD_FEATURE_TOGGLE,
67 | payload: {...toggle}
68 | });
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remote Config Dashboard
2 | [](https://travis-ci.org/joaoflf/remote-config-dashboard)
3 |
4 | Angular Dashboard to manage feature toggles and remote configuration.
5 |
6 | ## About
7 | Feature toggles are becoming a necessity for organizations that wish to release in a continuous fashion. They allow for the decoupling of technical and product releases, rapid failover recovery and if extended even cool stuff like A/B testing.
8 | There are some great commercial options to respond to this need. However, for a simple implementation a basic service with a list of toggles and their states would suffice.
9 |
10 | This project was born out of the necessity for such simple implementation.
11 | It comprises of a web interface to manage feature toggles and other configurable remote properties that one might need in a web or native mobile app. This web app is meant to be plugged to an web service that would return app configuration, allow to post new configuration, etc.
12 |
13 | It is meant to be open to be modified in order to suit different needs.
14 |
15 | Check out a live demo [here](https://joaoflf.github.io/remote-config-dashboard/).
16 |
17 | 
18 |
19 | ## App & State Management
20 | This web app is built using Angular 4 with the [angular-cli](https://github.com/angular/angular-cli).
21 | It also uses [ngrx](https://github.com/ngrx) to manage its state, so you can use [redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension) to inspect it.
22 | The idea is for the app to manage all its state offline and when the user wishes, he can press a *publish* button to synchronize the config json with the service.
23 | [ngrx/effects](https://github.com/ngrx/effects) used to connect to a mock api for fetching data.
24 |
25 | A [Remote Config API](https://github.com/joaoflf/remote-config-api) is also being built to support the web app.
26 |
27 | ## Development
28 | Run `npm start` to launch the web server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
29 | The web app is using a mock api provided by mockable.io
30 |
31 | ## Next steps
32 | * Sync Button
33 | * Loading States
34 |
35 | ## Credits
36 | In order to build this dashboard, some amazing open source projects and components were used. These include [ngrx](https://github.com/ngrx), [ClarityUI](https://vmware.github.io/clarity/) and [JSONEditor](https://github.com/josdejong/jsoneditor)
37 |
38 |
--------------------------------------------------------------------------------
/src/app/feature-toggles/feature-toggles.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { LOAD_TOGGLES_SUCCESS } from '../state-management/feature-toggles';
2 | import { Store } from '@ngrx/store';
3 | import { AppModule } from '../';
4 | /* tslint:disable:no-unused-variable */
5 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
6 | import { By } from '@angular/platform-browser';
7 | import { FeatureTogglesComponent } from './feature-toggles.component';
8 |
9 | describe('FeatureTogglesComponent', () => {
10 | let component: FeatureTogglesComponent;
11 | let fixture: ComponentFixture;
12 |
13 | beforeEach(async(() => {
14 | TestBed.configureTestingModule({
15 | imports: [AppModule]
16 | })
17 | .compileComponents()
18 | .then(() => {
19 | fixture = TestBed.createComponent(FeatureTogglesComponent);
20 | let store = fixture.debugElement.injector.get(Store);
21 | store.dispatch({
22 | type: LOAD_TOGGLES_SUCCESS,
23 | payload: [
24 | {
25 | name: 'testToggle',
26 | state: false
27 | }
28 | ]
29 | });
30 | component = fixture.componentInstance;
31 | fixture.detectChanges();
32 | });
33 | }));
34 |
35 | it('should create the component and view elements', () => {
36 | expect(component).toBeTruthy();
37 | let element = fixture.nativeElement;
38 | expect(element.querySelector('.toggles-table')).toBeTruthy();
39 | expect(element.querySelector('.toggle-search-input')).toBeTruthy();
40 | expect(element.querySelector('.new-toggle-button')).toBeTruthy();
41 | });
42 |
43 | it('should get featureToggles from store and render them in the table', () => {
44 | component.featureToggles$.subscribe((toggles) => {
45 | expect(toggles[0].name).toBe('testToggle');
46 | });
47 |
48 | let element = fixture.nativeElement;
49 | expect(element.querySelector('.toggle-name-cell').innerHTML).toBe('testToggle');
50 | });
51 |
52 | it('should remove a toggle with confirmation modal', () => {
53 | component.openRemovalConfirmationModal('testToggle');
54 | expect(component.confirmationMessage).toBe('Are you sure you want to remove the toggle testToggle?');
55 | expect(component.isConfirmationModalOpen).toBeTruthy();
56 |
57 | component.confirmToggleRemoval();
58 | component.featureToggles$.subscribe((toggles) => {
59 | expect(toggles.length).toBe(0);
60 | });
61 | });
62 |
63 | it('should update a toggle', () => {
64 | component.updateFeatureToggle('testToggle', true);
65 | component.featureToggles$.subscribe((toggles) => {
66 | expect(toggles[0].state).toBeTruthy();
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/src/app/config-properties/config-properties.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { LOAD_PROPERTIES_SUCCESS, UPDATE_PROPERTIES } from '../state-management/properties';
2 | import { Store } from '@ngrx/store';
3 | import { AppModule } from '../';
4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
5 | import { ConfigPropertiesComponent } from './config-properties.component';
6 |
7 | describe('ConfigPropertiesComponent', () => {
8 | let component: ConfigPropertiesComponent;
9 | let fixture: ComponentFixture;
10 | let store: Store