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 (
+
+ );
+ }
+}
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 (
+
+ );
+ }
+}
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) {