From 86a365bd406125498c1bbc45de2ee4d67f9fd0d5 Mon Sep 17 00:00:00 2001 From: sidmuls <52431759+sidmuls@users.noreply.github.com> Date: Thu, 24 Oct 2019 18:18:50 -0700 Subject: [PATCH] feat(provider/aws): Functions (listing and searching) (#7536) * . * feat(provider/aws): Function listing and searching functionality This change adds a new tab for functions and facilitates listing existing functions and searching functions listed. Corresponding provider specific changes are included in this change for AWS. Authors: @sidmuls and @nabebe * feat(provider/aws): Function listing and searching functionality This change adds a new tab for functions and facilitates listing existing functions and searching functions listed. Corresponding provider specific changes are included in this change for AWS. Authors: @sidmuls and @nabebe * refactor(provider/aws): Converting Function class component into function component. This is a refactoring change as requested by reviewer @caseyhebebrand Author: @sidmuls * refactor(provider/aws): Refactoring groupings into a separate component. This change is for refactoring requested by reviewer @caseyhebebrand. Author: @sidmuls * refactor(provider/aws): Removing unnecessary state from FunctionGroupings. This change is a refactoring requested by @caseyhebebrand Author: @sidmuls * refactor(provider/aws): removing extra line. Tthis is actually for retrying tests on TravisCI * refactor(provider/aws): Adding tests for FunctionFilterService and changes as requested by @christopherthielen Author: @sidmuls * refactor(provider/aws): removing unnecessary field from IFunction --- app/scripts/modules/amazon/src/aws.module.ts | 7 +- .../amazon/src/domain/IAmazonFunction.ts | 29 +++ .../modules/amazon/src/domain/index.ts | 1 + .../amazon/src/function/function.module.ts | 5 + .../src/function/function.transformer.ts | 7 + .../modules/amazon/src/function/index.ts | 1 + app/scripts/modules/amazon/src/index.ts | 1 + app/scripts/modules/core/src/core.module.ts | 4 +- .../modules/core/src/domain/IFunction.ts | 27 ++ app/scripts/modules/core/src/domain/index.ts | 2 +- .../core/src/entityTag/EntityTagsReader.ts | 25 +- .../modules/core/src/function/Function.tsx | 46 ++++ .../core/src/function/FunctionDetails.tsx | 11 + .../core/src/function/FunctionGroupings.tsx | 33 +++ .../modules/core/src/function/FunctionPod.tsx | 39 +++ .../modules/core/src/function/Functions.tsx | 106 ++++++++ .../function/filter/FunctionFilterModel.ts | 25 ++ .../filter/FunctionFilterService.spec.ts | 186 ++++++++++++++ .../function/filter/FunctionFilterService.ts | 121 +++++++++ .../src/function/filter/FunctionFilters.tsx | 240 ++++++++++++++++++ .../core/src/function/function.dataSource.ts | 43 ++++ .../core/src/function/function.module.ts | 8 + .../src/function/function.read.service.ts | 58 +++++ .../core/src/function/function.states.ts | 72 ++++++ .../core/src/function/function.transformer.js | 49 ++++ .../core/src/function/functionPod.less | 40 +++ .../modules/core/src/function/index.ts | 2 + app/scripts/modules/core/src/index.ts | 2 +- app/scripts/modules/core/src/state/index.ts | 11 + 29 files changed, 1195 insertions(+), 6 deletions(-) create mode 100644 app/scripts/modules/amazon/src/domain/IAmazonFunction.ts create mode 100644 app/scripts/modules/amazon/src/function/function.module.ts create mode 100644 app/scripts/modules/amazon/src/function/function.transformer.ts create mode 100644 app/scripts/modules/amazon/src/function/index.ts create mode 100644 app/scripts/modules/core/src/domain/IFunction.ts create mode 100644 app/scripts/modules/core/src/function/Function.tsx create mode 100644 app/scripts/modules/core/src/function/FunctionDetails.tsx create mode 100644 app/scripts/modules/core/src/function/FunctionGroupings.tsx create mode 100644 app/scripts/modules/core/src/function/FunctionPod.tsx create mode 100644 app/scripts/modules/core/src/function/Functions.tsx create mode 100644 app/scripts/modules/core/src/function/filter/FunctionFilterModel.ts create mode 100644 app/scripts/modules/core/src/function/filter/FunctionFilterService.spec.ts create mode 100644 app/scripts/modules/core/src/function/filter/FunctionFilterService.ts create mode 100644 app/scripts/modules/core/src/function/filter/FunctionFilters.tsx create mode 100644 app/scripts/modules/core/src/function/function.dataSource.ts create mode 100644 app/scripts/modules/core/src/function/function.module.ts create mode 100644 app/scripts/modules/core/src/function/function.read.service.ts create mode 100644 app/scripts/modules/core/src/function/function.states.ts create mode 100644 app/scripts/modules/core/src/function/function.transformer.js create mode 100644 app/scripts/modules/core/src/function/functionPod.less create mode 100644 app/scripts/modules/core/src/function/index.ts diff --git a/app/scripts/modules/amazon/src/aws.module.ts b/app/scripts/modules/amazon/src/aws.module.ts index 1887783f3d9..682a33036e3 100644 --- a/app/scripts/modules/amazon/src/aws.module.ts +++ b/app/scripts/modules/amazon/src/aws.module.ts @@ -3,6 +3,7 @@ import { module } from 'angular'; import { CloudProviderRegistry, DeploymentStrategyRegistry } from '@spinnaker/core'; import { AWS_LOAD_BALANCER_MODULE } from './loadBalancer/loadBalancer.module'; +import { AWS_FUNCTION_MODULE } from './function/function.module'; import { AWS_REACT_MODULE } from './reactShims/aws.react.module'; import { AWS_SECURITY_GROUP_MODULE } from './securityGroup/securityGroup.module'; import { AWS_SERVER_GROUP_TRANSFORMER } from './serverGroup/serverGroup.transformer'; @@ -17,7 +18,7 @@ import { AwsImageReader } from './image'; import { AmazonLoadBalancerClusterContainer } from './loadBalancer/AmazonLoadBalancerClusterContainer'; import { AmazonLoadBalancersTag } from './loadBalancer/AmazonLoadBalancersTag'; import { AwsLoadBalancerTransformer } from './loadBalancer/loadBalancer.transformer'; - +import { AwsFunctionTransformer } from './function/function.transformer'; import './deploymentStrategy/rollingPush.strategy'; import './logo/aws.logo.less'; @@ -73,6 +74,7 @@ module(AMAZON_MODULE, [ AWS_SERVER_GROUP_TRANSFORMER, require('./instance/awsInstanceType.service').name, AWS_LOAD_BALANCER_MODULE, + AWS_FUNCTION_MODULE, require('./instance/details/instance.details.controller').name, AWS_SECURITY_GROUP_MODULE, SUBNET_RENDERER, @@ -127,6 +129,9 @@ module(AMAZON_MODULE, [ ClusterContainer: AmazonLoadBalancerClusterContainer, LoadBalancersTag: AmazonLoadBalancersTag, }, + function: { + transformer: AwsFunctionTransformer, + }, securityGroup: { transformer: 'awsSecurityGroupTransformer', reader: 'awsSecurityGroupReader', diff --git a/app/scripts/modules/amazon/src/domain/IAmazonFunction.ts b/app/scripts/modules/amazon/src/domain/IAmazonFunction.ts new file mode 100644 index 00000000000..40a37c5532f --- /dev/null +++ b/app/scripts/modules/amazon/src/domain/IAmazonFunction.ts @@ -0,0 +1,29 @@ +import { IFunction } from '@spinnaker/core'; + +export interface IAmazonFunction extends IFunction { + credentials?: string; + role?: string; + runtime: string; + s3bucket: string; + s3key: string; + handler: string; + functionName: string; + publish: boolean; + description: string; + tags: [{}]; + memorySize: number; + timeout: number; + envVariables: {}; + tracingConfig: { + mode: string; + }; + deadLetterConfig: { + targetArn: string; + }; + KMSKeyArn: string; + vpcConfig: { + securityGroupIds: []; + subnetIds: []; + vpcId: string; + }; +} diff --git a/app/scripts/modules/amazon/src/domain/index.ts b/app/scripts/modules/amazon/src/domain/index.ts index 6faadf7ed7b..12849b007df 100644 --- a/app/scripts/modules/amazon/src/domain/index.ts +++ b/app/scripts/modules/amazon/src/domain/index.ts @@ -1,6 +1,7 @@ export * from './IAmazonCertificate'; export * from './IAmazonInstance'; export * from './IAmazonLoadBalancer'; +export * from './IAmazonFunction'; export * from './IAmazonLoadBalancerSourceData'; export * from './IAmazonHealth'; export * from './IAmazonScalingPolicy'; diff --git a/app/scripts/modules/amazon/src/function/function.module.ts b/app/scripts/modules/amazon/src/function/function.module.ts new file mode 100644 index 00000000000..4c99e4b99d2 --- /dev/null +++ b/app/scripts/modules/amazon/src/function/function.module.ts @@ -0,0 +1,5 @@ +import { module } from 'angular'; + +export const AWS_FUNCTION_MODULE = 'spinnaker.amazon.function'; + +module(AWS_FUNCTION_MODULE, []); diff --git a/app/scripts/modules/amazon/src/function/function.transformer.ts b/app/scripts/modules/amazon/src/function/function.transformer.ts new file mode 100644 index 00000000000..f578374159b --- /dev/null +++ b/app/scripts/modules/amazon/src/function/function.transformer.ts @@ -0,0 +1,7 @@ +import { IAmazonFunction } from 'amazon/domain'; + +export class AwsFunctionTransformer { + public normalizeFunction(functionDef: IAmazonFunction): IAmazonFunction { + return functionDef; + } +} diff --git a/app/scripts/modules/amazon/src/function/index.ts b/app/scripts/modules/amazon/src/function/index.ts new file mode 100644 index 00000000000..77369a8c3fb --- /dev/null +++ b/app/scripts/modules/amazon/src/function/index.ts @@ -0,0 +1 @@ +export * from './function.transformer'; diff --git a/app/scripts/modules/amazon/src/index.ts b/app/scripts/modules/amazon/src/index.ts index 69b06d73dd2..f6c26e1099f 100644 --- a/app/scripts/modules/amazon/src/index.ts +++ b/app/scripts/modules/amazon/src/index.ts @@ -6,6 +6,7 @@ export * from './common'; export * from './domain'; export * from './keyPairs'; export * from './loadBalancer'; +export * from './function'; export * from './reactShims'; export * from './serverGroup'; export * from './templates'; diff --git a/app/scripts/modules/core/src/core.module.ts b/app/scripts/modules/core/src/core.module.ts index 5a69c135ccf..4d69cef48b6 100644 --- a/app/scripts/modules/core/src/core.module.ts +++ b/app/scripts/modules/core/src/core.module.ts @@ -46,7 +46,7 @@ import { INSIGHT_MODULE } from './insight/insight.module'; import { INTERCEPTOR_MODULE } from './interceptor/interceptor.module'; import { LOAD_BALANCER_MODULE } from './loadBalancer/loadBalancer.module'; import { MANAGED_RESOURCE_CONFIG } from './application/config/managedResources/ManagedResourceConfig'; - +import { FUNCTION_MODULE } from './function/function.module'; import { NETWORK_INTERCEPTOR } from './api/network.interceptor'; import { PAGE_TITLE_MODULE } from './pageTitle/pageTitle.module'; @@ -114,7 +114,7 @@ module(CORE_MODULE, [ LABEL_FILTER_COMPONENT, LOAD_BALANCER_MODULE, - + FUNCTION_MODULE, MANAGED_RESOURCE_CONFIG, require('./modal/modal.module').name, diff --git a/app/scripts/modules/core/src/domain/IFunction.ts b/app/scripts/modules/core/src/domain/IFunction.ts new file mode 100644 index 00000000000..3b5fdfb6ed4 --- /dev/null +++ b/app/scripts/modules/core/src/domain/IFunction.ts @@ -0,0 +1,27 @@ +import { ITaggedEntity } from './ITaggedEntity'; + +export interface IFunctionSourceData { + cloudProvider?: string; + name?: string; + provider?: string; + type?: string; +} + +export interface IFunction extends ITaggedEntity { + account?: string; + cloudProvider?: string; + description?: string; + functionName?: string; + region?: string; + searchField?: string; + type?: string; + vpcId?: string; + vpcName?: string; +} + +export interface IFunctionGroup { + heading: string; + functionDef?: IFunction; + subgroups?: IFunctionGroup[]; + searchField?: string; +} diff --git a/app/scripts/modules/core/src/domain/index.ts b/app/scripts/modules/core/src/domain/index.ts index c5f2e34070e..af7649983df 100644 --- a/app/scripts/modules/core/src/domain/index.ts +++ b/app/scripts/modules/core/src/domain/index.ts @@ -19,7 +19,7 @@ export * from './IExecution'; export * from './IExecutionStage'; export * from './IExecutionTrigger'; export * from './IExpectedArtifact'; - +export * from './IFunction'; export * from './IHealth'; export * from './IInstance'; diff --git a/app/scripts/modules/core/src/entityTag/EntityTagsReader.ts b/app/scripts/modules/core/src/entityTag/EntityTagsReader.ts index 771e0c96ed8..2decd822737 100644 --- a/app/scripts/modules/core/src/entityTag/EntityTagsReader.ts +++ b/app/scripts/modules/core/src/entityTag/EntityTagsReader.ts @@ -5,7 +5,15 @@ import { $q } from 'ngimport'; import { API } from 'core/api/ApiService'; import { IEntityTags, IEntityTag, ICreationMetadataTag } from '../domain/IEntityTags'; import { Application } from 'core/application/application.model'; -import { IExecution, IPipeline, IServerGroup, IServerGroupManager, ILoadBalancer, ISecurityGroup } from 'core/domain'; +import { + IExecution, + IPipeline, + IServerGroup, + IServerGroupManager, + ILoadBalancer, + ISecurityGroup, + IFunction, +} from 'core/domain'; import { SETTINGS } from 'core/config/settings'; export class EntityTagsReader { @@ -71,6 +79,21 @@ export class EntityTagsReader { }); } + public static addTagsToFunctions(application: Application): void { + if (!SETTINGS.feature.entityTags) { + return; + } + const allTags = application.getDataSource('entityTags').data; + const functionTags: IEntityTags[] = allTags.filter(t => t.entityRef.entityType === 'function'); + application.getDataSource('functions').data.forEach((fn: IFunction) => { + fn.entityTags = functionTags.find( + t => + t.entityRef.entityId === fn.functionName && + t.entityRef.account === fn.account && + t.entityRef.region === fn.region, + ); + }); + } public static addTagsToSecurityGroups(application: Application): void { if (!SETTINGS.feature.entityTags) { return; diff --git a/app/scripts/modules/core/src/function/Function.tsx b/app/scripts/modules/core/src/function/Function.tsx new file mode 100644 index 00000000000..d2afa1380cf --- /dev/null +++ b/app/scripts/modules/core/src/function/Function.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { UISref, UISrefActive } from '@uirouter/react'; +import { Application } from 'core/application/application.model'; +import { IFunction } from 'core/domain'; +import { EntityNotifications } from 'core/entityTag/notifications/EntityNotifications'; + +interface IFunctionProps { + application: Application; + functionDef: IFunction; +} + +const Function = (props: IFunctionProps) => { + const { application, functionDef } = props; + const params = { + application: application.name, + region: functionDef.region, + account: functionDef.account, + name: functionDef.functionName, + provider: functionDef.cloudProvider, + }; + return ( +
+
+ + +
+ +   {(functionDef.region || '').toUpperCase()} +
+ application.functions.refresh()} + /> +
+
+
+
+
+
+ ); +}; +export default Function; diff --git a/app/scripts/modules/core/src/function/FunctionDetails.tsx b/app/scripts/modules/core/src/function/FunctionDetails.tsx new file mode 100644 index 00000000000..a876a3f4722 --- /dev/null +++ b/app/scripts/modules/core/src/function/FunctionDetails.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Overridable, IOverridableProps } from 'core/overrideRegistry'; + +export interface IFunctionDetailsProps extends IOverridableProps {} + +@Overridable('function.details') +export class FunctionDetails extends React.Component { + public render() { + return

Function Details

; + } +} diff --git a/app/scripts/modules/core/src/function/FunctionGroupings.tsx b/app/scripts/modules/core/src/function/FunctionGroupings.tsx new file mode 100644 index 00000000000..15ad08626f2 --- /dev/null +++ b/app/scripts/modules/core/src/function/FunctionGroupings.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { Application } from '@spinnaker/core'; +import { IFunctionGroup } from 'core/domain'; +import { FunctionPod } from './FunctionPod'; + +export interface IFunctionGroupingsProps { + app: Application; + groups: IFunctionGroup[]; +} +export class FunctionGroupings extends React.Component { + constructor(props: IFunctionGroupingsProps) { + super(props); + } + public render() { + return ( +
+ {this.props.groups.map(group => ( +
+ {group.subgroups && + group.subgroups.map(subgroup => ( + + ))} +
+ ))} +
+ ); + } +} diff --git a/app/scripts/modules/core/src/function/FunctionPod.tsx b/app/scripts/modules/core/src/function/FunctionPod.tsx new file mode 100644 index 00000000000..c9515d20600 --- /dev/null +++ b/app/scripts/modules/core/src/function/FunctionPod.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; + +import { AccountTag } from 'core/account'; +import { Application } from 'core/application/application.model'; +import { IFunctionGroup } from 'core/domain'; +import Function from './Function'; + +import './functionPod.less'; + +export interface IFunctionPodProps { + grouping: IFunctionGroup; + application: Application; + parentHeading: string; +} + +export class FunctionPod extends React.Component { + public render(): React.ReactElement { + const { grouping, application, parentHeading } = this.props; + const subgroups = grouping.subgroups.map(subgroup => ( + + )); + + return ( +
+
+
+
+ +
+
+
{grouping.heading}
+
+
+
+
{subgroups}
+
+ ); + } +} diff --git a/app/scripts/modules/core/src/function/Functions.tsx b/app/scripts/modules/core/src/function/Functions.tsx new file mode 100644 index 00000000000..ea5b12064b9 --- /dev/null +++ b/app/scripts/modules/core/src/function/Functions.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { Debounce } from 'lodash-decorators'; +import { Subscription } from 'rxjs'; +import { Application } from 'core/application/application.model'; +import { FilterTags, IFilterTag } from 'core/filterModel/FilterTags'; +import { IFunctionGroup } from 'core/domain'; +import { FunctionState } from 'core/state'; +import { Spinner } from 'core/widgets/spinners/Spinner'; +import { FunctionGroupings } from './FunctionGroupings'; + +export interface IFunctionsProps { + app: Application; +} + +export interface IFunctionsState { + initialized: boolean; + groups: IFunctionGroup[]; + tags: IFilterTag[]; +} + +export class Functions extends React.Component { + private groupsUpdatedListener: Subscription; + private functionsRefreshUnsubscribe: () => any; + + constructor(props: IFunctionsProps) { + super(props); + this.state = { + initialized: false, + groups: [], + tags: [], + }; + } + + public componentDidMount(): void { + const { app } = this.props; + + this.groupsUpdatedListener = FunctionState.filterService.groupsUpdatedStream.subscribe(() => this.groupsUpdated()); + FunctionState.filterModel.asFilterModel.activate(); + this.functionsRefreshUnsubscribe = app + .getDataSource('functions') + .onRefresh(null, () => this.updateFunctionGroups()); + app.setActiveState(app.loadBalancers); + this.updateFunctionGroups(); + } + + public componentWillUnmount(): void { + this.groupsUpdatedListener.unsubscribe(); + this.functionsRefreshUnsubscribe(); + } + + private groupsUpdated(): void { + this.setState({ + groups: FunctionState.filterModel.asFilterModel.groups, + tags: FunctionState.filterModel.asFilterModel.tags, + }); + } + + @Debounce(200) + private updateFunctionGroups(): void { + FunctionState.filterModel.asFilterModel.applyParamsToUrl(); + FunctionState.filterService.updateFunctionGroups(this.props.app); + this.groupsUpdated(); + + if (this.props.app.getDataSource('functions').loaded) { + this.setState({ initialized: true }); + } + } + + private clearFilters = (): void => { + FunctionState.filterService.clearFilters(); + this.updateFunctionGroups(); + }; + + private tagCleared = (): void => { + this.updateFunctionGroups(); + }; + + public render(): React.ReactElement { + const groupings = this.state.initialized ? ( +
+ + {this.state.groups.length === 0 && ( +
+

No functions match the filters you've selected.

+
+ )} +
+ ) : ( +
+ +
+ ); + + return ( +
+
+
+
+
+ +
+
{groupings}
+
+ ); + } +} diff --git a/app/scripts/modules/core/src/function/filter/FunctionFilterModel.ts b/app/scripts/modules/core/src/function/filter/FunctionFilterModel.ts new file mode 100644 index 00000000000..d6bc66f8c15 --- /dev/null +++ b/app/scripts/modules/core/src/function/filter/FunctionFilterModel.ts @@ -0,0 +1,25 @@ +import { IFunctionGroup } from 'core/domain'; +import { IFilterConfig, IFilterModel } from 'core/filterModel/IFilterModel'; +import { FilterModelService } from 'core/filterModel'; + +export const filterModelConfig: IFilterConfig[] = [ + { model: 'account', param: 'acct', type: 'trueKeyObject' }, + { model: 'detail', param: 'detail', type: 'trueKeyObject' }, + { model: 'filter', param: 'q', clearValue: '', type: 'string', filterLabel: 'search' }, + { model: 'providerType', type: 'trueKeyObject', filterLabel: 'provider' }, + { model: 'region', param: 'reg', type: 'trueKeyObject' }, +]; + +export interface IFunctionFilterModel extends IFilterModel { + groups: IFunctionGroup[]; +} + +export class FunctionFilterModel { + public asFilterModel: IFunctionFilterModel; + + constructor() { + this.asFilterModel = FilterModelService.configureFilterModel(this as any, filterModelConfig); + FilterModelService.registerRouterHooks(this.asFilterModel, '**.application.insight.functions.**'); + this.asFilterModel.activate(); + } +} diff --git a/app/scripts/modules/core/src/function/filter/FunctionFilterService.spec.ts b/app/scripts/modules/core/src/function/filter/FunctionFilterService.spec.ts new file mode 100644 index 00000000000..9926e75a3a3 --- /dev/null +++ b/app/scripts/modules/core/src/function/filter/FunctionFilterService.spec.ts @@ -0,0 +1,186 @@ +import { Application } from 'core/application/application.model'; +import { ApplicationModelBuilder } from 'core/application/applicationModel.builder'; +import { FunctionState } from 'core/state'; +import { groupBy } from 'lodash'; + +// Most of this logic has been moved to filter.model.service.js, so these act more as integration tests +describe('Service: functionFilterService', function() { + const debounceTimeout = 30; + + let app: Application, resultJson: any; + + beforeEach(() => { + FunctionState.filterModel.asFilterModel.groups = []; + }); + + beforeEach(function() { + app = ApplicationModelBuilder.createApplicationForTests('app', { key: 'functions', lazy: true }); + app.getDataSource('functions').data = [ + { + functionName: 'function1', + region: 'us-east-1', + account: 'test', + }, + { + functionName: 'function2', + region: 'us-west-1', + account: 'prod', + }, + { + functionName: 'function3', + region: 'us-east-1', + account: 'test', + }, + ]; + + resultJson = [ + { heading: 'us-east-1', functionDef: app.functions.data[0] }, + { heading: 'us-west-1', functionDef: app.functions.data[1] }, + { heading: 'us-east-1', functionDef: app.functions.data[2] }, + ]; + FunctionState.filterModel.asFilterModel.clearFilters(); + }); + + describe('Updating the function group', function() { + it('no filter: should be transformed', function(done) { + const expected = [ + { + heading: 'prod', + subgroups: [{ heading: 'function2', subgroups: [resultJson[1]] }], + }, + { + heading: 'test', + subgroups: [ + { heading: 'function1', subgroups: [resultJson[0]] }, + { heading: 'function3', subgroups: [resultJson[2]] }, + ], + }, + ]; + FunctionState.filterService.updateFunctionGroups(app); + setTimeout(() => { + expect(FunctionState.filterModel.asFilterModel.groups).toEqual(expected); + done(); + }, debounceTimeout); + }); + + describe('filtering by account type', function() { + it('1 account filter: should be transformed showing only prod accounts', function(done) { + FunctionState.filterModel.asFilterModel.sortFilter.account = { prod: true }; + FunctionState.filterService.updateFunctionGroups(app); + + setTimeout(() => { + expect(FunctionState.filterModel.asFilterModel.groups).toEqual([ + { + heading: 'prod', + subgroups: [{ heading: 'function2', subgroups: [resultJson[1]] }], + }, + ]); + done(); + }, debounceTimeout); + }); + + it('All account filters: should show all accounts', function(done) { + FunctionState.filterModel.asFilterModel.sortFilter.account = { prod: true, test: true }; + FunctionState.filterService.updateFunctionGroups(app); + + setTimeout(() => { + expect(FunctionState.filterModel.asFilterModel.groups).toEqual([ + { + heading: 'prod', + subgroups: [{ heading: 'function2', subgroups: [resultJson[1]] }], + }, + { + heading: 'test', + subgroups: [ + { heading: 'function1', subgroups: [resultJson[0]] }, + { heading: 'function3', subgroups: [resultJson[2]] }, + ], + }, + ]); + done(); + }, debounceTimeout); + }); + }); + }); + + describe('filter by region', function() { + it('1 region: should filter by that region', function(done) { + FunctionState.filterModel.asFilterModel.sortFilter.region = { 'us-east-1': true }; + FunctionState.filterService.updateFunctionGroups(app); + + setTimeout(() => { + expect(FunctionState.filterModel.asFilterModel.groups).toEqual([ + { + heading: 'test', + subgroups: [ + { heading: 'function1', subgroups: [resultJson[0]] }, + { heading: 'function3', subgroups: [resultJson[2]] }, + ], + }, + ]); + done(); + }, debounceTimeout); + }); + + it('All regions: should show all functions', function(done) { + FunctionState.filterModel.asFilterModel.sortFilter.region = { 'us-east-1': true, 'us-west-1': true }; + FunctionState.filterService.updateFunctionGroups(app); + + setTimeout(() => { + expect(FunctionState.filterModel.asFilterModel.groups).toEqual([ + { + heading: 'prod', + subgroups: [{ heading: 'function2', subgroups: [resultJson[1]] }], + }, + { + heading: 'test', + subgroups: [ + { heading: 'function1', subgroups: [resultJson[0]] }, + { heading: 'function3', subgroups: [resultJson[2]] }, + ], + }, + ]); + done(); + }, debounceTimeout); + }); + }); + + it('Filter by region: filterFunctionsForDisplay', function(done) { + FunctionState.filterModel.asFilterModel.sortFilter.region = { 'us-west-1': true }; + const functionsToDisplay = FunctionState.filterService.filterFunctionsForDisplay(app.functions.data); + setTimeout(() => { + expect(functionsToDisplay).toEqual([resultJson[1].functionDef]); + done(); + }, debounceTimeout); + }); + + describe('function with same name and different regions ', function() { + it('grouped with region in heading', function(done) { + const newFunction = { + functionName: 'function1', + account: 'test', + region: 'eu-west-1', + }; + app.functions.data.push(newFunction); + const groupedByAccount = groupBy(app.functions.data, 'account'); + const groups = FunctionState.filterService.getFunctionGroups(groupedByAccount); + setTimeout(() => { + expect(groups).toEqual([ + { + heading: 'test', + subgroups: [ + { heading: 'function1 (eu-west-1)', subgroups: [{ heading: 'eu-west-1', functionDef: newFunction }] }, + { heading: 'function1 (us-east-1)', subgroups: [resultJson[0]] }, + { heading: 'function3', subgroups: [resultJson[2]] }, + ], + }, + { + heading: 'prod', + subgroups: [{ heading: 'function2', subgroups: [resultJson[1]] }], + }, + ]); + done(); + }, debounceTimeout); + }); + }); +}); diff --git a/app/scripts/modules/core/src/function/filter/FunctionFilterService.ts b/app/scripts/modules/core/src/function/filter/FunctionFilterService.ts new file mode 100644 index 00000000000..28809542481 --- /dev/null +++ b/app/scripts/modules/core/src/function/filter/FunctionFilterService.ts @@ -0,0 +1,121 @@ +import { chain, forOwn, groupBy, intersection, sortBy, values, Dictionary } from 'lodash'; +import { Debounce } from 'lodash-decorators'; +import { Subject } from 'rxjs'; + +import { Application } from 'core/application/application.model'; +import { FilterModelService } from 'core/filterModel'; +import { IFunction, IFunctionGroup } from 'core/domain'; +import { FunctionState } from 'core/state'; + +export class FunctionFilterService { + public groupsUpdatedStream: Subject = new Subject(); + + private lastApplication: Application; + + constructor() {} + + private addSearchFields(functionDef: IFunction): void { + if (!functionDef.searchField) { + functionDef.searchField = [functionDef.functionName, functionDef.region.toLowerCase(), functionDef.account].join( + ' ', + ); + } + } + + private checkSearchTextFilter(functionDef: IFunction): boolean { + const filter = FunctionState.filterModel.asFilterModel.sortFilter.filter; + if (!filter) { + return true; + } + + if (filter.includes('vpc:')) { + const [, vpcName] = /vpc:([\w-]*)/.exec(filter); + return functionDef.vpcName.toLowerCase() === vpcName.toLowerCase(); + } + this.addSearchFields(functionDef); + return filter.split(' ').every((testWord: string) => { + return functionDef.searchField.includes(testWord); + }); + } + + public filterFunctionsForDisplay(functions: IFunction[]): IFunction[] { + return chain(functions) + .filter(fn => this.checkSearchTextFilter(fn)) + .filter(fn => FilterModelService.checkAccountFilters(FunctionState.filterModel.asFilterModel)(fn)) + .filter(fn => FilterModelService.checkRegionFilters(FunctionState.filterModel.asFilterModel)(fn)) + .filter(fn => FilterModelService.checkProviderFilters(FunctionState.filterModel.asFilterModel)(fn)) + .value(); + } + + public sortGroupsByHeading(): void { + // sort groups in place so Angular doesn't try to update the world + FunctionState.filterModel.asFilterModel.groups.sort((a, b) => { + return a.heading.localeCompare(b.heading); + }); + } + + public clearFilters(): void { + FunctionState.filterModel.asFilterModel.clearFilters(); + FunctionState.filterModel.asFilterModel.applyParamsToUrl(); + } + + public getFunctionGroups(groupedByAccount: Dictionary): IFunctionGroup[] { + const groups: IFunctionGroup[] = []; + forOwn(groupedByAccount, (group, account) => { + const groupedByRegion = values(groupBy(group, 'region')); + const namesByRegion = groupedByRegion.map(g => g.map(fn => fn.functionName)); + /** gather functions with same name but different region */ + const functionNames = + namesByRegion.length > 1 + ? intersection(...namesByRegion).reduce<{ [key: string]: boolean }>((acc, name) => { + acc[name] = true; + return acc; + }, {}) + : {}; + /* Group by functionName:region */ + const subGroupings = groupBy(group, fn => `${fn.functionName}:${fn.region}`), + subGroups: IFunctionGroup[] = []; + + forOwn(subGroupings, (subGroup, nameAndRegion) => { + const [name, region] = nameAndRegion.split(':'); + const subSubGroups: IFunctionGroup[] = []; + + subGroup.forEach(functionDef => { + subSubGroups.push({ + heading: functionDef.region, + functionDef, + }); + }); + + /* In case function with same name exists in a different region, heading = name(region)*/ + const heading = `${name}${functionNames[name] && region ? ` (${region})` : ''}`; + subGroups.push({ + heading, + subgroups: sortBy(subSubGroups, 'heading'), + }); + }); + groups.push({ heading: account, subgroups: sortBy(subGroups, 'heading') }); + }); + return groups; + } + + @Debounce(25) + public updateFunctionGroups(application: Application): void { + if (!application) { + application = this.lastApplication; + if (!this.lastApplication) { + return null; + } + } + + const functions = this.filterFunctionsForDisplay(application.functions.data); + const grouped = groupBy(functions, 'account'); + const groups = this.getFunctionGroups(grouped); + + FunctionState.filterModel.asFilterModel.groups = groups; + this.sortGroupsByHeading(); + FunctionState.filterModel.asFilterModel.addTags(); + this.lastApplication = application; + this.groupsUpdatedStream.next(groups); + } +} diff --git a/app/scripts/modules/core/src/function/filter/FunctionFilters.tsx b/app/scripts/modules/core/src/function/filter/FunctionFilters.tsx new file mode 100644 index 00000000000..c6f64140ba2 --- /dev/null +++ b/app/scripts/modules/core/src/function/filter/FunctionFilters.tsx @@ -0,0 +1,240 @@ +import * as React from 'react'; +import { chain, compact, debounce, uniq, map } from 'lodash'; +import { $rootScope } from 'ngimport'; +import { Subscription } from 'rxjs'; + +import { Application } from 'core/application'; +import { CloudProviderLabel, CloudProviderLogo } from 'core/cloudProvider'; +import { FilterCollapse, ISortFilter, digestDependentFilters } from 'core/filterModel'; +import { FilterSection } from 'core/cluster/filter/FilterSection'; +import { FunctionState } from 'core/state'; + +const poolValueCoordinates = [ + { filterField: 'providerType', on: 'function', localField: 'type' }, + { filterField: 'account', on: 'function', localField: 'account' }, + { filterField: 'region', on: 'function', localField: 'region' }, +]; + +function poolBuilder(functions: any[]) { + const pool = chain(functions) + .map(fn => { + const poolUnits = chain(poolValueCoordinates) + .filter({ on: 'function' }) + .reduce( + (acc, coordinate) => { + acc[coordinate.filterField] = fn[coordinate.localField]; + return acc; + }, + {} as any, + ) + .value(); + return poolUnits; + }) + .flatten() + .value(); + return pool; +} + +export interface IFunctionFiltersProps { + app: Application; +} + +export interface IFunctionFiltersState { + sortFilter: ISortFilter; + tags: any[]; + providerTypeHeadings: string[]; + accountHeadings: string[]; + regionHeadings: string[]; +} + +export class FunctionFilters extends React.Component { + private debouncedUpdateFunctionGroups: () => void; + private groupsUpdatedSubscription: Subscription; + private functionsRefreshUnsubscribe: () => void; + private locationChangeUnsubscribe: () => void; + + constructor(props: IFunctionFiltersProps) { + super(props); + this.state = { + sortFilter: FunctionState.filterModel.asFilterModel.sortFilter, + tags: FunctionState.filterModel.asFilterModel.tags, + providerTypeHeadings: [], + accountHeadings: [], + regionHeadings: [], + }; + + this.debouncedUpdateFunctionGroups = debounce(this.updateFunctionGroups, 300); + } + + public componentDidMount(): void { + const { app } = this.props; + + this.groupsUpdatedSubscription = FunctionState.filterService.groupsUpdatedStream.subscribe(() => { + this.setState({ tags: FunctionState.filterModel.asFilterModel.tags }); + }); + + if (app.functions && app.functions.loaded) { + this.updateFunctionGroups(); + } + + this.functionsRefreshUnsubscribe = app.functions.onRefresh(null, () => this.updateFunctionGroups()); + + this.locationChangeUnsubscribe = $rootScope.$on('$locationChangeSuccess', () => { + FunctionState.filterModel.asFilterModel.activate(); + FunctionState.filterService.updateFunctionGroups(app); + }); + } + + public componentWillUnmount(): void { + this.groupsUpdatedSubscription.unsubscribe(); + this.functionsRefreshUnsubscribe(); + this.locationChangeUnsubscribe(); + } + + public updateFunctionGroups = (applyParamsToUrl = true): void => { + const { app } = this.props; + + if (applyParamsToUrl) { + FunctionState.filterModel.asFilterModel.applyParamsToUrl(); + } + FunctionState.filterService.updateFunctionGroups(app); + + const { region, account } = digestDependentFilters({ + sortFilter: FunctionState.filterModel.asFilterModel.sortFilter, + dependencyOrder: ['providerType', 'account', 'region'], + pool: poolBuilder(app.functions.data), + }); + + this.setState({ + accountHeadings: account, + regionHeadings: region, + providerTypeHeadings: this.getHeadingsForOption('type'), + }); + }; + + private getHeadingsForOption = (option: string): string[] => { + return compact(uniq(map(this.props.app.functions.data, option) as string[])).sort(); + }; + + private clearFilters = (): void => { + FunctionState.filterService.clearFilters(); + FunctionState.filterModel.asFilterModel.applyParamsToUrl(); + FunctionState.filterService.updateFunctionGroups(this.props.app); + }; + + private handleSearchBlur = (event: React.ChangeEvent) => { + const target = event.target; + this.state.sortFilter.filter = target.value; + this.updateFunctionGroups(); + }; + + private handleSearchChange = (event: React.ChangeEvent) => { + const target = event.target; + this.state.sortFilter.filter = target.value; + this.setState({ sortFilter: this.state.sortFilter }); + this.debouncedUpdateFunctionGroups(); + }; + + public render() { + const fuctionsLoaded = this.props.app.functions.loaded; + const { accountHeadings, providerTypeHeadings, regionHeadings, sortFilter, tags } = this.state; + + return ( +
+ +
+ 0 ? 'inherit' : 'hidden' }} + onClick={this.clearFilters} + > + Clear All + + +
+
+ +
+
+
+
+ {fuctionsLoaded && ( +
+ {providerTypeHeadings.length > 1 && ( + + {providerTypeHeadings.map(heading => ( + + ))} + + )} + + + {accountHeadings.map(heading => ( + + ))} + + + + {regionHeadings.map(heading => ( + + ))} + +
+ )} +
+ ); + } +} + +const FilterCheckbox = (props: { + heading: string; + sortFilterType: { [key: string]: boolean }; + onChange: () => void; + isCloudProvider?: boolean; +}): JSX.Element => { + const { heading, isCloudProvider, onChange, sortFilterType } = props; + const changeHandler = (event: React.ChangeEvent) => { + const target = event.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + sortFilterType[heading] = Boolean(value); + onChange(); + }; + return ( +
+ +
+ ); +}; diff --git a/app/scripts/modules/core/src/function/function.dataSource.ts b/app/scripts/modules/core/src/function/function.dataSource.ts new file mode 100644 index 00000000000..5e1c86683f7 --- /dev/null +++ b/app/scripts/modules/core/src/function/function.dataSource.ts @@ -0,0 +1,43 @@ +import { module, IQService } from 'angular'; + +import { ApplicationDataSourceRegistry } from 'core/application/service/ApplicationDataSourceRegistry'; +import { INFRASTRUCTURE_KEY } from 'core/application/nav/defaultCategories'; +import { Application } from 'core/application/application.model'; +import { EntityTagsReader } from 'core/entityTag/EntityTagsReader'; +import { IFunction } from 'core/domain'; +import { FUNCTION_READ_SERVICE, FunctionReader } from 'core/function/function.read.service'; + +export const FUNCTION_DATA_SOURCE = 'spinnaker.core.functions.dataSource'; +module(FUNCTION_DATA_SOURCE, [FUNCTION_READ_SERVICE]).run([ + '$q', + 'functionReader', + ($q: IQService, functionReader: FunctionReader) => { + const functions = (application: Application) => { + return functionReader.loadFunctions(application.name); + }; + + const addFunctions = (_application: Application, functionList: IFunction[]) => { + return $q.when(functionList); + }; + + const addTags = (application: Application) => { + EntityTagsReader.addTagsToFunctions(application); + }; + + ApplicationDataSourceRegistry.registerDataSource({ + key: 'functions', + label: 'functions', + sref: '.insight.functions', + category: INFRASTRUCTURE_KEY, + optional: true, + icon: 'fa fa-xs fa-fw icon-sitemap', + loader: functions, + onLoad: addFunctions, + afterLoad: addTags, + providerField: 'cloudProvider', + credentialsField: 'account', + regionField: 'region', + description: 'Serverless Compute Service.', + }); + }, +]); diff --git a/app/scripts/modules/core/src/function/function.module.ts b/app/scripts/modules/core/src/function/function.module.ts new file mode 100644 index 00000000000..f83abb73086 --- /dev/null +++ b/app/scripts/modules/core/src/function/function.module.ts @@ -0,0 +1,8 @@ +import { module } from 'angular'; + +import { FUNCTION_DATA_SOURCE } from './function.dataSource'; +import { FUNCTION_STATES } from './function.states'; + +export const FUNCTION_MODULE = 'spinnaker.core.function'; + +module(FUNCTION_MODULE, [FUNCTION_DATA_SOURCE, FUNCTION_STATES]); diff --git a/app/scripts/modules/core/src/function/function.read.service.ts b/app/scripts/modules/core/src/function/function.read.service.ts new file mode 100644 index 00000000000..0e285d6cd4f --- /dev/null +++ b/app/scripts/modules/core/src/function/function.read.service.ts @@ -0,0 +1,58 @@ +import { IPromise, IQService, module } from 'angular'; + +import { API } from 'core/api/ApiService'; +import { IFunctionSourceData, IFunction } from 'core/domain'; + +export interface IFunctionByAccount { + name: string; + accounts: Array<{ + name: string; + regions: Array<{ + name: string; + functions: IFunctionSourceData[]; + }>; + }>; +} + +export class FunctionReader { + public static $inject = ['$q', 'functionTransformer']; + public constructor(private $q: IQService, private functionTransformer: any) {} + + public loadFunctions(applicationName: string): IPromise { + return API.one('applications', applicationName) + .all('functions') + .getList() + .then((functions: IFunctionSourceData[]) => { + functions = this.functionTransformer.normalizeFunctionSet(functions); + return this.$q.all(functions.map(fn => this.normalizeFunction(fn))); + }); + } + + public getFunctionDetails( + cloudProvider: string, + account: string, + region: string, + name: string, + ): IPromise { + return API.all('functions') + .withParams({ provider: cloudProvider, functionName: name, region: region, account: account }) + .get(); + } + + public listFunctions(cloudProvider: string): IPromise { + return API.all('functions') + .withParams({ provider: cloudProvider }) + .getList(); + } + + private normalizeFunction(functionDef: IFunctionSourceData): IPromise { + return this.functionTransformer.normalizeFunction(functionDef).then((fn: IFunction) => { + fn.cloudProvider = fn.cloudProvider || 'aws'; + return fn; + }); + } +} + +export const FUNCTION_READ_SERVICE = 'spinnaker.core.function.read.service'; + +module(FUNCTION_READ_SERVICE, [require('./function.transformer').name]).service('functionReader', FunctionReader); diff --git a/app/scripts/modules/core/src/function/function.states.ts b/app/scripts/modules/core/src/function/function.states.ts new file mode 100644 index 00000000000..ee3124d3543 --- /dev/null +++ b/app/scripts/modules/core/src/function/function.states.ts @@ -0,0 +1,72 @@ +import { module } from 'angular'; +import { StateParams } from '@uirouter/angularjs'; + +import { INestedState, StateConfigProvider } from 'core/navigation'; +import { APPLICATION_STATE_PROVIDER, ApplicationStateProvider } from 'core/application'; +import { filterModelConfig } from 'core/function/filter/FunctionFilterModel'; +import { Functions } from 'core/function/Functions'; + +import { FunctionDetails } from './FunctionDetails'; +import { FunctionFilters } from './filter/FunctionFilters'; + +export const FUNCTION_STATES = 'spinnaker.core.functions.states'; +module(FUNCTION_STATES, [APPLICATION_STATE_PROVIDER]).config([ + 'applicationStateProvider', + 'stateConfigProvider', + (applicationStateProvider: ApplicationStateProvider, stateConfigProvider: StateConfigProvider) => { + const functionDetails: INestedState = { + name: 'functionDetails', + url: '/functionDetails/:provider/:account/:region/:name', + views: { + 'detail@../insight': { + component: FunctionDetails, + $type: 'react', + }, + }, + resolve: { + accountId: ['$stateParams', ($stateParams: StateParams) => $stateParams.account], + functionObj: [ + '$stateParams', + ($stateParams: StateParams) => { + return { + name: $stateParams.name, + accountId: $stateParams.account, + region: $stateParams.region, + vpcId: $stateParams.vpcId, + }; + }, + ], + }, + data: { + pageTitleDetails: { + title: 'Function Details', + nameParam: 'functionName', + accountParam: 'credentials', + regionParam: 'region', + }, + history: { + type: 'functions', + }, + }, + }; + + const functions: INestedState = { + url: `/functions?${stateConfigProvider.paramsToQuery(filterModelConfig)}`, + name: 'functions', + views: { + nav: { component: FunctionFilters, $type: 'react' }, + master: { component: Functions, $type: 'react' }, + }, + params: stateConfigProvider.buildDynamicParams(filterModelConfig), + data: { + pageTitleSection: { + title: 'Functions', + }, + }, + children: [], + }; + + applicationStateProvider.addInsightState(functions); + applicationStateProvider.addInsightDetailState(functionDetails); + }, +]); diff --git a/app/scripts/modules/core/src/function/function.transformer.js b/app/scripts/modules/core/src/function/function.transformer.js new file mode 100644 index 00000000000..5af7aa8ef8d --- /dev/null +++ b/app/scripts/modules/core/src/function/function.transformer.js @@ -0,0 +1,49 @@ +'use strict'; + +const angular = require('angular'); + +import { chain, flow } from 'lodash'; + +import { AccountService } from 'core/account/AccountService'; +import { PROVIDER_SERVICE_DELEGATE } from 'core/cloudProvider/providerService.delegate'; + +module.exports = angular + .module('spinnaker.core.function.transformer', [PROVIDER_SERVICE_DELEGATE]) + .factory('functionTransformer', [ + 'providerServiceDelegate', + function(providerServiceDelegate) { + function normalizeFunction(functionDef) { + return AccountService.getAccountDetails(functionDef.account).then(accountDetails => { + return providerServiceDelegate + .getDelegate( + functionDef.provider ? functionDef.provider : 'aws', + 'function.transformer', + accountDetails && accountDetails.skin, + ) + .normalizeFunction(functionDef); + }); + } + + function normalizeFunctionSet(functions) { + let setNormalizers = chain(functions) + .filter(fn => + providerServiceDelegate.hasDelegate(fn.provider ? fn.provider : 'aws', 'function.setTransformer'), + ) + .compact() + .map(fn => providerServiceDelegate.getDelegate(fn.provider, 'function.setTransformer').normalizeFunctionSet) + .uniq() + .value(); + + if (setNormalizers.length) { + return flow(setNormalizers)(functions); + } else { + return functions; + } + } + + return { + normalizeFunction: normalizeFunction, + normalizeFunctionSet: normalizeFunctionSet, + }; + }, + ]); diff --git a/app/scripts/modules/core/src/function/functionPod.less b/app/scripts/modules/core/src/function/functionPod.less new file mode 100644 index 00000000000..7c0b5fbf406 --- /dev/null +++ b/app/scripts/modules/core/src/function/functionPod.less @@ -0,0 +1,40 @@ +@import (reference) '~core/presentation/less/imports/commonImports.less'; + +.function-pod { + .cluster-container { + padding: 3px 15px; + &.disabled { + opacity: 0.4; + &:active, + &:hover { + opacity: 0.7; + } + } + } + .icon-sitemap { + color: var(--color-success); + } + .rollup-details { + .pod-subgroup { + h6 { + padding-bottom: 10px; + background: rgba(0, 0, 0, 0); + } + } + } + .function-header { + overflow: hidden; + background-color: var(--color-alabaster); + padding: 8px 20px 3px 20px; + .health-counts { + .instance-health-counts { + margin-top: -2px; + } + } + } + .function { + > .cluster-container { + padding-top: 0; + } + } +} diff --git a/app/scripts/modules/core/src/function/index.ts b/app/scripts/modules/core/src/function/index.ts new file mode 100644 index 00000000000..ee0ff5667c8 --- /dev/null +++ b/app/scripts/modules/core/src/function/index.ts @@ -0,0 +1,2 @@ +export * from './FunctionDetails'; +export * from './function.read.service'; diff --git a/app/scripts/modules/core/src/index.ts b/app/scripts/modules/core/src/index.ts index 15a37081393..11a873758b8 100644 --- a/app/scripts/modules/core/src/index.ts +++ b/app/scripts/modules/core/src/index.ts @@ -41,7 +41,7 @@ export * from './image'; export * from './instance'; export * from './loadBalancer'; - +export * from './function'; export * from './modal'; export * from './managed'; diff --git a/app/scripts/modules/core/src/state/index.ts b/app/scripts/modules/core/src/state/index.ts index 2cf49e6ce7e..e2a9f999429 100644 --- a/app/scripts/modules/core/src/state/index.ts +++ b/app/scripts/modules/core/src/state/index.ts @@ -8,6 +8,8 @@ import { LoadBalancerFilterService } from 'core/loadBalancer/filter/LoadBalancer import { MultiselectModel } from 'core/cluster/filter/MultiselectModel'; import { SecurityGroupFilterModel } from 'core/securityGroup/filter/SecurityGroupFilterModel'; import { SecurityGroupFilterService } from 'core/securityGroup/filter/SecurityGroupFilterService'; +import { FunctionFilterModel } from 'core/function/filter/FunctionFilterModel'; +import { FunctionFilterService } from 'core/function/filter/FunctionFilterService'; export interface IStateCluster { filterModel: ClusterFilterModel; @@ -29,10 +31,16 @@ export interface IStateSecurityGroup { filterService: SecurityGroupFilterService; } +export interface IStateFunctions { + filterModel: FunctionFilterModel; + filterService: FunctionFilterService; +} + export const ClusterState = {} as IStateCluster; export const ExecutionState = {} as IStateExecution; export const LoadBalancerState = {} as IStateLoadBalancers; export const SecurityGroupState = {} as IStateSecurityGroup; +export const FunctionState = {} as IStateFunctions; export function initialize(): void { ClusterState.filterModel = new ClusterFilterModel(); @@ -44,6 +52,9 @@ export function initialize(): void { LoadBalancerState.filterModel = new LoadBalancerFilterModel(); LoadBalancerState.filterService = new LoadBalancerFilterService(); + FunctionState.filterModel = new FunctionFilterModel(); + FunctionState.filterService = new FunctionFilterService(); + SecurityGroupState.filterModel = new SecurityGroupFilterModel(); SecurityGroupState.filterService = new SecurityGroupFilterService(); if (SETTINGS.checkForUpdates) {