Skip to content

Commit

Permalink
Feature/add root table (#1568)
Browse files Browse the repository at this point in the history
* feature flag root table section basic setup

* add constants

---------

Co-authored-by: Zack Lee <[email protected]>
  • Loading branch information
danoswaltCL and zackcl authored May 22, 2024
1 parent 42820e7 commit 6fa3ed8
Show file tree
Hide file tree
Showing 17 changed files with 371 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,95 @@ import { Inject, Injectable } from '@angular/core';
import { ENV, Environment } from '../../../environments/environment-types';
import { HttpClient } from '@angular/common/http';
import { FeatureFlagsPaginationParams } from './store/feature-flags.model';
import { delay, of } from 'rxjs';
import { FEATURE_FLAG_STATUS, FILTER_MODE } from '../../../../../../../types/src';

@Injectable()
export class FeatureFlagsDataService {
constructor(private http: HttpClient, @Inject(ENV) private environment: Environment) {}

fetchFeatureFlagsPaginated(params: FeatureFlagsPaginationParams) {
const url = this.environment.api.getPaginatedFlags;
return this.http.post(url, params);
// return this.http.post(url, params);
// mock
return of({ nodes: mockFeatureFlags, total: 2 }).pipe(delay(2000));
}
}

const mockFeatureFlags = [
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '1',
name: 'Feature Flag 1',
key: 'feature_flag_1',
description: 'Feature Flag 1 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '2',
name: 'Feature Flag 2',
key: 'feature_flag_2',
description: 'Feature Flag 2 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '3',
name: 'Feature Flag 2',
key: 'feature_flag_2',
description: 'Feature Flag 2 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '4',
name: 'Feature Flag 4',
key: 'feature_flag_4',
description: 'Feature Flag 4 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '5',
name: 'Feature Flag 5',
key: 'feature_flag_5',
description: 'Feature Flag 5 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,21 @@ import {
selectAllFeatureFlagsSortedByDate,
selectIsAllFlagsFetched,
selectIsLoadingFeatureFlags,
selectHasInitialFeatureFlagsDataLoaded,
} from './store/feature-flags.selectors';
import * as FeatureFlagsActions from './store/feature-flags.actions';
import { FEATURE_FLAG_STATUS, FILTER_MODE, FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types';
import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, SORT_AS_DIRECTION } from 'upgrade_types';

@Injectable()
export class FeatureFlagsService {
constructor(private store$: Store<AppState>) {}

isInitialFeatureFlagsLoading$ = this.store$.pipe(select(selectHasInitialFeatureFlagsDataLoaded));
isLoadingFeatureFlags$ = this.store$.pipe(select(selectIsLoadingFeatureFlags));
allFeatureFlags$ = this.store$.pipe(select(selectAllFeatureFlagsSortedByDate));
isAllFlagsFetched$ = this.store$.pipe(select(selectIsAllFlagsFetched));

fetchFeatureFlags(fromStarting?: boolean) {
this.store$.dispatch(FeatureFlagsActions.actionFetchFeatureFlags({ fromStarting }));

// mock response
setTimeout(() => {
this.store$.dispatch(
FeatureFlagsActions.actionFetchFeatureFlagsSuccess({
flags: mockFeatureFlags,
totalFlags: 1,
})
);
}, 3000);
}

setSearchKey(searchKey: FLAG_SEARCH_KEY) {
Expand All @@ -46,39 +37,4 @@ export class FeatureFlagsService {
setSortingType(sortingType: SORT_AS_DIRECTION) {
this.store$.dispatch(FeatureFlagsActions.actionSetSortingType({ sortingType }));
}

//**** mocks
}

const mockFeatureFlags = [
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '1',
name: 'Feature Flag 1',
key: 'feature_flag_1',
description: 'Feature Flag 1 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
{
createdAt: '2021-09-08T08:00:00.000Z',
updatedAt: '2021-09-08T08:00:00.000Z',
versionNumber: 1,
id: '1',
name: 'Feature Flag 2',
key: 'feature_flag_2',
description: 'Feature Flag 2 Description',
status: FEATURE_FLAG_STATUS.ENABLED,
filterMode: FILTER_MODE.INCLUDE_ALL,
context: ['context1', 'context2'],
tags: ['tag1', 'tag2'],
featureFlagSegmentInclusion: null,
featureFlagSegmentExclusion: null,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,29 @@ export enum FLAG_SEARCH_KEY {
CONTEXT = 'context',
}

export const FLAG_ROOT_COLUMN_NAMES = {
NAME: 'Name',
STATUS: 'Status',
UPDATED_AT: 'Updated at',
APP_CONTEXT: 'App Context',
TAGS: 'Tags',
EXPOSURES: 'Exposures',
};

export const FLAG_TRANSLATION_KEYS = {
NAME: 'feature-flags.global-name.text',
STATUS: 'feature-flags.global-status.text',
UPDATED_AT: 'feature-flags.global-updated-at.text',
APP_CONTEXT: 'feature-flags.global-app-context.text',
TAGS: 'feature-flags.global-tags.text',
EXPOSURES: 'feature-flags.global-exposures.text',
};

export const FLAG_ROOT_DISPLAYED_COLUMNS = Object.values(FLAG_ROOT_COLUMN_NAMES);

export interface FeatureFlagState extends EntityState<FeatureFlag> {
isLoadingFeatureFlags: boolean;
hasInitialFeatureFlagsDataLoaded: boolean;
skipFlags: number;
totalFlags: number;
searchKey: FLAG_SEARCH_KEY;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const { selectIds, selectEntities, selectAll, selectTotal } = adapter.get

export const initialState: FeatureFlagState = adapter.getInitialState({
isLoadingFeatureFlags: false,
hasInitialFeatureFlagsDataLoaded: false,
skipFlags: 0,
totalFlags: 0,
totalFlags: null,
searchKey: FLAG_SEARCH_KEY.ALL,
searchString: null,
sortKey: null,
Expand All @@ -24,12 +25,16 @@ const reducer = createReducer(
isLoadingFeatureFlags: true,
})),
on(FeatureFlagsActions.actionFetchFeatureFlagsSuccess, (state, { flags, totalFlags }) => {
const newState = {
const newState: FeatureFlagState = {
...state,
totalFlags,
skipFlags: state.skipFlags + flags.length,
};
return adapter.upsertMany(flags, { ...newState, isLoadingFeatureFlags: false });
return adapter.upsertMany(flags, {
...newState,
isLoadingFeatureFlags: false,
hasInitialFeatureFlagsDataLoaded: true,
});
}),
on(FeatureFlagsActions.actionFetchFeatureFlagsFailure, (state) => ({ ...state, isLoadingFeatureFlags: false })),
on(FeatureFlagsActions.actionSetIsLoadingFeatureFlags, (state, { isLoadingFeatureFlags }) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export const selectAllFeatureFlagsSortedByDate = createSelector(selectAllFeature
});
});

export const selectHasInitialFeatureFlagsDataLoaded = createSelector(
selectFeatureFlagsState,
(state) => state.hasInitialFeatureFlagsDataLoaded
);

export const selectIsLoadingFeatureFlags = createSelector(
selectFeatureFlagsState,
(state) => state.isLoadingFeatureFlags
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonSectionCardListComponent } from '../../../../../../shared-standalone-component-lib/components';
import { FeatureFlagRootSectionCardComponent } from '../feature-flag-root-section-card/feature-flag-root-section-card.component';
import { FeatureFlagRootSectionCardComponent } from './feature-flag-root-section-card/feature-flag-root-section-card.component';

@Component({
selector: 'app-feature-flag-root-page-content',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<div class="flags-list-container">
<div scroll (scrolled)="fetchFlagsOnScroll()" class="flags-list-table-container" #tableContainer>
<mat-progress-bar class="spinner" mode="indeterminate" *ngIf="isLoading$ | async"></mat-progress-bar>
<table class="flags-list" mat-table [dataSource]="dataSource$" matSort (matSortChange)="changeSorting($event)">
<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.NAME">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.NAME | translate" matTooltipPosition="above">
{{ FLAG_TRANSLATION_KEYS.NAME | translate | uppercase }}
</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">
<span>{{ flag.name }}</span>
<ng-template #flagNameEllipsis>
<span
[matTooltip]="flag.name"
class="flag-name",
matTooltipPosition="above"
>
{{ flag.name | truncate: 30 }}
</span>
</ng-template>
<br />
<span class="flag-description ft-10-400" *ngIf="flag.description?.length < 30; else flagDescription">
{{ flag.description }}
</span>
<ng-template #flagDescription>
<span class="flag-description ft-10-400" [matTooltip]="flag.description" matTooltipPosition="above">
{{ flag.description | truncate: 35 }}
</span>
</ng-template>
</td>
</ng-container>

<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.STATUS">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.STATUS | translate" matTooltipPosition="above">{{ FLAG_TRANSLATION_KEYS.STATUS | translate | uppercase }}</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">{{ flag.status }}</td>
</ng-container>

<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.UPDATED_AT">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.UPDATED_AT | translate" matTooltipPosition="above">
{{ FLAG_TRANSLATION_KEYS.UPDATED_AT | translate | uppercase }}
</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">
{{ flag.updatedAt }}
</td>
</ng-container>

<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.APP_CONTEXT">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.APP_CONTEXT | translate" matTooltipPosition="above">
{{ FLAG_TRANSLATION_KEYS.APP_CONTEXT | translate | uppercase }}
</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">
{{ flag.context[0] }}
</td>
</ng-container>

<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.TAGS">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.TAGS | translate" matTooltipPosition="above">{{ FLAG_TRANSLATION_KEYS.TAGS | translate | uppercase }}</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">
<mat-chip *ngFor="let tag of flag.tags" class="tag">{{ tag }}</mat-chip>
</td>
</ng-container>

<ng-container [matColumnDef]="FLAG_ROOT_COLUMN_NAMES.EXPOSURES">
<th class="ft-12-700" mat-header-cell *matHeaderCellDef mat-sort-header>
<span [matTooltip]="FLAG_TRANSLATION_KEYS.EXPOSURES | translate" matTooltipPosition="above">
{{ FLAG_TRANSLATION_KEYS.EXPOSURES | translate | uppercase }}
</span>
</th>
<td class="ft-12-600" mat-cell *matCellDef="let flag">
<span *ngFor="let exposure of flag.exposures" class="exposure">{{ exposure }}</span>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
:host ::ng-deep .flags-list-container {
height: 100%;
width: 100%;

.flags-list-table-container {
position: relative;
margin-top: 8px;
padding: 0 2.25rem;
overflow: auto;
width: 100%;

.spinner {
position: sticky;
top: 0;
z-index: 1111;
}
}

.flags-list {
width: 100%;

th {
color: var(--grey-2);
}

tr.mat-mdc-footer-row,
tr.mat-mdc-row {
height: 55px;
}

.mat-mdc-cell,
.mat-mdc-header-cell {
padding: 10px 5px;
width: 10%;
word-break: break-word;
}

.mat-mdc-header-cell {
padding-left: 5px;
}

.flag {
&-name {
text-decoration: underline;
cursor: pointer;
}

&-description {
color: var(--grey-3);
}
}

.mat-mdc-form-field {
width: 40%;
}
}
}
Loading

0 comments on commit 6fa3ed8

Please sign in to comment.