,
},
"Type": "Custom::S3Unzip",
},
"Urls": {
"Properties": {
"Client": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"ApiUrl",
"Name",
],
},
"/static/client.html",
],
],
},
"Designer": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"ApiUrl",
"Name",
],
},
"/static/index.html",
],
],
},
"OpenSearchDashboards": {
"Fn::Sub": "\${ESVar.ESAddress}/_dashboards/app/dashboards#/list",
},
"ServiceToken": {
"Fn::GetAtt": [
"CFNLambda",
"Arn",
],
},
},
"Type": "Custom::Variable",
},
"User": {
"DependsOn": [
"SignupPermision",
"MessagePermision",
"OpenSearchDashboardsRoleAttachment",
"RoleAttachment",
],
"Properties": {
"DesiredDeliveryMediums": [
"EMAIL",
],
"UserAttributes": [
{
"Name": "email",
"Value": {
"Ref": "Email",
},
},
],
"UserPoolId": {
"Ref": "UserPool",
},
"Username": {
"Ref": "Username",
},
},
"Type": "AWS::Cognito::UserPoolUser",
},
"UserPool": {
"Properties": {
"AdminCreateUserConfig": {
"AllowAdminCreateUserOnly": {
"Fn::If": [
"AdminSignUp",
true,
false,
],
},
"InviteMessageTemplate": {
"EmailMessage": {
"Fn::Sub": "Hello {username},
Welcome to QnABot! Your temporary password is:
{####}
When the CloudFormation stack is COMPLETE, use the link below to log in to QnABot Content Designer, set your permanent password, and start building your bot!
\${ApiUrl.Name}/pages/designer
Good luck!
QnABot (www.amazon.com/qnabot)
",
},
"EmailSubject": "Welcome to QnABot!",
},
},
"AliasAttributes": [
"email",
],
"AutoVerifiedAttributes": [
"email",
],
"LambdaConfig": {
"CustomMessage": {
"Fn::GetAtt": [
"MessageLambda",
"Arn",
],
},
"PreSignUp": {
"Fn::GetAtt": [
"SignupLambda",
"Arn",
],
},
},
"Schema": [
{
"AttributeDataType": "String",
"Mutable": true,
"Name": "email",
"Required": true,
},
],
"UserPoolName": {
"Fn::Join": [
"-",
[
"UserPool",
{
"Ref": "AWS::StackName",
},
],
],
},
},
"Type": "AWS::Cognito::UserPool",
},
"UserRole": {
"Metadata": {
"guard": {
"SuppressedRules": [
"IAM_NO_INLINE_POLICY_CHECK",
"CFN_NO_EXPLICIT_RESOURCE_NAMES",
],
},
},
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated",
},
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "IdPool",
},
},
},
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"Path": "/",
"Policies": [
{
"Fn::If": [
"StreamingEnabled",
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"execute-api:Invoke",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Fn::Sub": "\${AWS::Partition}",
},
":execute-api:",
{
"Fn::Sub": "\${AWS::Region}",
},
":",
{
"Fn::Sub": "\${AWS::AccountId}",
},
":",
{
"Fn::GetAtt": [
"StreamingStack",
"Outputs.StreamingWebSocketApiId",
],
},
"/Prod/*",
],
],
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "StreamingApiAccess",
},
{
"Ref": "AWS::NoValue",
},
],
},
],
"RoleName": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
"0",
{
"Fn::Split": [
"-",
{
"Fn::Select": [
2,
{
"Fn::Split": [
"/",
{
"Ref": "AWS::StackId",
},
],
},
],
},
],
},
],
},
"-UserRole",
],
],
},
},
"Type": "AWS::IAM::Role",
},
"UserToGroup": {
"Properties": {
"GroupName": {
"Ref": "Admins",
},
"UserPoolId": {
"Ref": "UserPool",
},
"Username": {
"Ref": "User",
},
},
"Type": "AWS::Cognito::UserPoolUserToGroupAttachment",
},
"Users": {
"Properties": {
"GroupName": "Users",
"UserPoolId": {
"Ref": "UserPool",
},
},
"Type": "AWS::Cognito::UserPoolGroup",
},
"UsersTable": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W74",
"reason": "This DynamoDB table does not require CMK encryption store in KMS",
},
],
},
},
"Properties": {
"AttributeDefinitions": [
{
"AttributeName": "UserId",
"AttributeType": "S",
},
],
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "UserId",
"KeyType": "HASH",
},
],
"PointInTimeRecoverySpecification": {
"PointInTimeRecoveryEnabled": true,
},
"TimeToLiveSpecification": {
"AttributeName": "ttl",
"Enabled": true,
},
},
"Type": "AWS::DynamoDB::Table",
},
"UtteranceLambda": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W92",
"reason": "This lambda function does not require to have ReservedConcurrentExecutions",
},
],
},
"guard": {
"SuppressedRules": [
"LAMBDA_CONCURRENCY_CHECK",
"LAMBDA_INSIDE_VPC",
],
},
},
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "BootstrapBucket",
},
"S3Key": {
"Fn::Sub": "\${BootstrapPrefix}/lambda/proxy-es.zip",
},
"S3ObjectVersion": {
"Ref": "ESProxyCodeVersion",
},
},
"Environment": {
"Variables": {
"ES_ADDRESS": {
"Fn::Join": [
"",
[
"https://",
{
"Fn::GetAtt": [
"ESVar",
"ESAddress",
],
},
],
],
},
"ES_INDEX": {
"Fn::GetAtt": [
"Var",
"QnaIndex",
],
},
"SOLUTION_ID": "SO0189",
"SOLUTION_VERSION": "vx.x.x",
"UTTERANCE_BUCKET": {
"Ref": "AssetBucket",
},
"UTTERANCE_KEY": "default-utterances.json",
},
},
"Handler": "index.utterances",
"Layers": [
{
"Ref": "AwsSdkLayerLambdaLayer",
},
{
"Ref": "CommonModulesLambdaLayer",
},
{
"Ref": "EsProxyLambdaLayer",
},
{
"Ref": "QnABotCommonLambdaLayer",
},
],
"LoggingConfig": {
"LogGroup": {
"Ref": "UtteranceLambdaLogGroup",
},
},
"MemorySize": "1408",
"Role": {
"Fn::GetAtt": [
"ESProxyLambdaRole",
"Arn",
],
},
"Runtime": "nodejs",
"Tags": [
{
"Key": "Type",
"Value": "Service",
},
],
"Timeout": 300,
"TracingConfig": {
"Fn::If": [
"XRAYEnabled",
{
"Mode": "Active",
},
{
"Ref": "AWS::NoValue",
},
],
},
"VpcConfig": {
"Fn::If": [
"VPCEnabled",
{
"SecurityGroupIds": {
"Ref": "VPCSecurityGroupIdList",
},
"SubnetIds": {
"Ref": "VPCSubnetIdList",
},
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Lambda::Function",
},
"UtteranceLambdaLogGroup": {
"Metadata": {
"guard": {
"SuppressedRules": [
"CLOUDWATCH_LOG_GROUP_ENCRYPTED",
"CW_LOGGROUP_RETENTION_PERIOD_CHECK",
],
},
},
"Properties": {
"LogGroupName": {
"Fn::Join": [
"-",
[
{
"Fn::Sub": "/aws/lambda/\${AWS::StackName}-UtteranceLambda",
},
{
"Fn::Select": [
"2",
{
"Fn::Split": [
"/",
{
"Ref": "AWS::StackId",
},
],
},
],
},
],
],
},
"RetentionInDays": {
"Fn::If": [
"LogRetentionPeriodIsNotZero",
{
"Ref": "LogRetentionPeriod",
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Logs::LogGroup",
},
"Var": {
"Properties": {
"FeedbackIndex": {
"op": "toLowerCase",
"value": {
"Fn::Sub": "\${AWS::StackName}-feedback",
},
},
"MetricsIndex": {
"op": "toLowerCase",
"value": {
"Fn::Sub": "\${AWS::StackName}-metrics",
},
},
"QnAType": "qna",
"QnaIndex": {
"op": "toLowerCase",
"value": {
"Fn::Sub": "\${AWS::StackName}",
},
},
"QuizType": "quiz",
"ResponseBotStackName": {
"op": "toLowerCase",
"value": {
"Fn::Sub": "\${AWS::StackName}-examples",
},
},
"ServiceToken": {
"Fn::GetAtt": [
"CFNLambda",
"Arn",
],
},
"index": {
"op": "toLowerCase",
"value": {
"Ref": "AWS::StackName",
},
},
},
"Type": "Custom::Variable",
},
"VersionLambda": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W92",
"reason": "This lambda function does not require to have ReservedConcurrentExecutions",
},
],
},
"guard": {
"SuppressedRules": [
"LAMBDA_CONCURRENCY_CHECK",
"LAMBDA_INSIDE_VPC",
],
},
},
"Properties": {
"Code": {
"ZipFile": "/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3');
const region = process.env.AWS_REGION;
const client = new S3Client({
customUserAgent: [
[\`AWSSOLUTION/\${process.env.SOLUTION_ID}/\${process.env.SOLUTION_VERSION}\`],
[\`AWSSOLUTION-CAPABILITY/\${process.env.SOLUTION_ID}-C023/\${process.env.SOLUTION_VERSION}\`],
],
region,
});
const SUCCESS = 'SUCCESS';
const FAILED = 'FAILED';
const https = require('https');
const { URL } = require('url');
async function send(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
return new Promise((resolve, reject) => {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: \`See the details in CloudWatch Log Stream: \${context.logStreamName}\`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData,
});
console.log('Response body:\\n', responseBody);
const parsedUrl = new URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname + parsedUrl.search,
method: 'PUT',
headers: {
'content-type': '',
'content-length': responseBody.length,
},
};
const request = https.request(options, (response) => {
console.log(\`Status code: \${response.statusCode}\`);
console.log(\`Status message: \${response.statusMessage}\`);
response.on('end', () => {
resolve();
});
});
request.on('error', (error) => {
console.log(\`send(..) failed executing https.request(..): \${error}\`);
reject(error);
});
request.write(responseBody);
request.end();
});
}
exports.handler = async function (event, context) {
console.log(JSON.stringify(event, null, 2));
if (event.RequestType !== 'Delete') {
const params = {
Bucket: event.ResourceProperties.Bucket,
Key: event.ResourceProperties.Key,
};
const headObjCmd = new HeadObjectCommand(params);
try {
const result = await client.send(headObjCmd);
await send(event, context, SUCCESS, {
version: result.VersionId ? result.VersionId : 1,
});
} catch (e) {
console.log(e);
await send(event, context, FAILED);
}
} else {
await send(event, context, SUCCESS);
}
context.done();
};
",
},
"Environment": {
"Variables": {
"SOLUTION_ID": "SO0189",
"SOLUTION_VERSION": "vx.x.x",
},
},
"Handler": "index.handler",
"LoggingConfig": {
"LogGroup": {
"Ref": "VersionLambdaLogGroup",
},
},
"MemorySize": "3008",
"Role": {
"Fn::GetAtt": [
"CFNLambdaRole",
"Arn",
],
},
"Runtime": "nodejs",
"Tags": [
{
"Key": "Type",
"Value": "CustomResource",
},
],
"Timeout": 60,
"TracingConfig": {
"Fn::If": [
"XRAYEnabled",
{
"Mode": "Active",
},
{
"Ref": "AWS::NoValue",
},
],
},
"VpcConfig": {
"Fn::If": [
"VPCEnabled",
{
"SecurityGroupIds": {
"Ref": "VPCSecurityGroupIdList",
},
"SubnetIds": {
"Ref": "VPCSubnetIdList",
},
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Lambda::Function",
},
"VersionLambdaLogGroup": {
"Metadata": {
"guard": {
"SuppressedRules": [
"CLOUDWATCH_LOG_GROUP_ENCRYPTED",
"CW_LOGGROUP_RETENTION_PERIOD_CHECK",
],
},
},
"Properties": {
"LogGroupName": {
"Fn::Join": [
"-",
[
{
"Fn::Sub": "/aws/lambda/\${AWS::StackName}-VersionLambda",
},
{
"Fn::Select": [
"2",
{
"Fn::Split": [
"/",
{
"Ref": "AWS::StackId",
},
],
},
],
},
],
],
},
"RetentionInDays": {
"Fn::If": [
"LogRetentionPeriodIsNotZero",
{
"Ref": "LogRetentionPeriod",
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Logs::LogGroup",
},
"WarmerLambdaRole": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W11",
"reason": "This IAM role requires to have * resource on its permission policy",
},
{
"id": "W12",
"reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray",
},
],
},
"guard": {
"SuppressedRules": [
"IAM_NO_INLINE_POLICY_CHECK",
],
},
},
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"Path": "/",
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":log-group:/aws/lambda/*",
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "LambdaFunctionServiceRolePolicy",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":log-group:/aws/lambda/*",
],
],
},
},
{
"Action": [
"ec2:CreateNetworkInterface",
"ec2:AssignPrivateIpAddresses",
"ec2:UnassignPrivateIpAddresses",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
},
"PolicyName": "lambdaVPCAccessExecutionRole",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries",
],
"Effect": "Allow",
"Resource": [
"*",
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "xrayDaemonWriteAccess",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"es:ESHttpGet",
],
"Effect": "Allow",
"Resource": [
"*",
],
"Sid": "AllowES",
},
],
"Version": "2012-10-17",
},
"PolicyName": "ParamStorePolicy",
},
],
},
"Type": "AWS::IAM::Role",
},
"dashboard": {
"Properties": {
"DashboardBody": {
"Fn::Sub": "{"widgets":[{"type":"text","width":24,"height":2,"x":0,"y":0,"properties":{"markdown":"# QnABot:\${AWS::StackName} Dashboard"}},{"type":"text","width":24,"height":2,"x":0,"y":3,"properties":{"markdown":"## OpenSearch"}},{"type":"metric","width":6,"height":6,"properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/ES","ReadLatency","DomainName","\${ESVar.ESDomain}","ClientId","\${AWS::AccountId}"]],"region":"\${AWS::Region}"},"x":0,"y":5},{"type":"metric","width":6,"height":6,"properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/ES","ReadIOPS","DomainName","\${ESVar.ESDomain}","ClientId","\${AWS::AccountId}"],[".","ReadThroughput",".",".",".",".",{"yAxis":"right"}]],"region":"\${AWS::Region}"},"x":6,"y":5},{"type":"metric","width":6,"height":6,"properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/ES","CPUUtilization","DomainName","\${ESVar.ESDomain}","ClientId","\${AWS::AccountId}"]],"region":"\${AWS::Region}"},"x":12,"y":5},{"type":"metric","x":18,"y":5,"properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/ES","ClusterUsedSpace","DomainName","\${ESVar.ESDomain}","ClientId","\${AWS::AccountId}"],[".","SearchableDocuments",".",".",".",".",{"yAxis":"right"}]],"region":"\${AWS::Region}"},"height":6,"width":6},{"type":"metric","width":6,"height":6,"properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/ES","ClusterStatus.green","DomainName","\${ESVar.ESDomain}","ClientId","\${AWS::AccountId}",{"color":"#2ca02c"}],[".","ClusterStatus.red",".",".",".",".",{"color":"#d62728"}],[".","ClusterStatus.yellow",".",".",".",".",{"color":"#bcbd22"}]],"region":"\${AWS::Region}"},"x":0,"y":11},{"type":"text","width":24,"height":2,"x":0,"y":24,"properties":{"markdown":"## Lambda Function"}},{"type":"text","width":24,"height":2,"x":0,"y":26,"properties":{"markdown":"### CustomResource"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${VersionLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"VersionLambda","period":300},"height":6,"width":6,"x":0,"y":28},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${CFNLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"CFNLambda","period":300},"height":6,"width":6,"x":6,"y":28},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESCFNProxyLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESCFNProxyLambda","period":300},"height":6,"width":6,"x":12,"y":28},{"type":"text","width":24,"height":2,"x":0,"y":34,"properties":{"markdown":"### Fulfillment"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${FulfillmentLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"FulfillmentLambda","period":300},"height":6,"width":6,"x":0,"y":36},{"type":"text","width":24,"height":2,"x":0,"y":42,"properties":{"markdown":"### Warmer"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESWarmerLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESWarmerLambda","period":300},"height":6,"width":6,"x":0,"y":44},{"type":"text","width":24,"height":2,"x":0,"y":50,"properties":{"markdown":"### Api"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${LexBuildLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"LexBuildLambda","period":300},"height":6,"width":6,"x":0,"y":52},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${LexBuildLambdaStart}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"LexBuildLambdaStart","period":300},"height":6,"width":6,"x":6,"y":52},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${LexBuildLambdaPoll}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"LexBuildLambdaPoll","period":300},"height":6,"width":6,"x":12,"y":52},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${Lexv2BotLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"Lexv2BotLambda","period":300},"height":6,"width":6,"x":18,"y":52},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${LexProxyLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"LexProxyLambda","period":300},"height":6,"width":6,"x":0,"y":58},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${LexStatusLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"LexStatusLambda","period":300},"height":6,"width":6,"x":6,"y":58},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${S3ListLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"S3ListLambda","period":300},"height":6,"width":6,"x":12,"y":58},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ExampleS3ListLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ExampleS3ListLambda","period":300},"height":6,"width":6,"x":18,"y":58},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ExampleS3ListPhotoLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ExampleS3ListPhotoLambda","period":300},"height":6,"width":6,"x":0,"y":64},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${SchemaLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"SchemaLambda","period":300},"height":6,"width":6,"x":6,"y":64},{"type":"text","width":24,"height":2,"x":0,"y":70,"properties":{"markdown":"### Service"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${UtteranceLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"UtteranceLambda","period":300},"height":6,"width":6,"x":0,"y":72},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESQidLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESQidLambda","period":300},"height":6,"width":6,"x":6,"y":72},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESCleaningLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESCleaningLambda","period":300},"height":6,"width":6,"x":12,"y":72},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESProxyLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESProxyLambda","period":300},"height":6,"width":6,"x":18,"y":72},{"type":"text","width":24,"height":2,"x":0,"y":78,"properties":{"markdown":"### Logging"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESLoggingLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESLoggingLambda","period":300},"height":6,"width":6,"x":0,"y":80},{"type":"text","width":24,"height":2,"x":0,"y":86,"properties":{"markdown":"### Query"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${ESQueryLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"ESQueryLambda","period":300},"height":6,"width":6,"x":0,"y":88},{"type":"text","width":24,"height":2,"x":0,"y":94,"properties":{"markdown":"### S3 Clean"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${S3Clean}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"S3Clean","period":300},"height":6,"width":6,"x":0,"y":96},{"type":"text","width":24,"height":2,"x":0,"y":102,"properties":{"markdown":"### Cognito"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${MessageLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"MessageLambda","period":300},"height":6,"width":6,"x":0,"y":104},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${SignupLambda}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"SignupLambda","period":300},"height":6,"width":6,"x":6,"y":104},{"type":"text","width":24,"height":2,"x":0,"y":110,"properties":{"markdown":"### Solution Helper"}},{"type":"metric","properties":{"view":"timeSeries","stacked":false,"metrics":[["AWS/Lambda","Errors","FunctionName","\${SolutionHelper}",{"stat":"Sum"}],[".","Invocations",".",".",{"stat":"Sum"}],[".","Duration",".",".",{"yAxis":"right"}],[".","Throttles",".",".",{"stat":"Sum"}]],"region":"\${AWS::Region}","title":"SolutionHelper","period":300},"height":6,"width":6,"x":0,"y":112}]}",
},
"DashboardName": {
"Fn::Sub": "\${AWS::Region}-\${AWS::StackName}",
},
},
"Type": "AWS::CloudWatch::Dashboard",
},
"export": {
"Properties": {
"ParentId": {
"Ref": "exports",
},
"PathPart": "{proxy+}",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"exportDelete": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "DELETE",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "DELETE",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "export",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"exportGet": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "GET",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status-export/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "export",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"exportPut": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "PUT",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "PUT",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set($inputRoot = $input.path('$'))
{
"bucket":"\${ExportBucket}",
"index":"\${Var.QnaIndex}",
"id":"$input.params('proxy')",
"config":"status/$input.params('proxy')",
"tmp":"tmp/$input.params('proxy')",
"key":"$inputRoot.get('prefix')data-export/$input.params('proxy')",
"filter":"$inputRoot.get('filter')",
"status":"Started"
}",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ExportBucket",
},
"/status-export/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "export",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"exports": {
"Properties": {
"ParentId": {
"Ref": "Jobs",
},
"PathPart": "exports",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"exportsList": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[InternalServiceError].*",
"StatusCode": 500,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[BadRequest].*",
"StatusCode": 400,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[Conflict].*",
"StatusCode": 409,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[NotFound].*",
"StatusCode": 404,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*Exception.*",
"StatusCode": 405,
},
],
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set ($root="https://\${!context.domainName}/\${!context.stage}")
{
"bucket":"\${ContentDesignerOutputBucket}",
"prefix":"status-export/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"exports",
"root":"$root"
}
",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"S3ListLambda",
"Arn",
],
},
"/invocations",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.date": true,
},
"StatusCode": 200,
},
{
"StatusCode": 404,
},
{
"StatusCode": 405,
},
{
"StatusCode": 500,
},
],
"RequestParameters": {
"method.request.querystring.perpage": false,
"method.request.querystring.token": false,
},
"ResourceId": {
"Ref": "exports",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"import": {
"Properties": {
"ParentId": {
"Ref": "imports",
},
"PathPart": "{proxy+}",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"importDelete": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "DELETE",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "DELETE",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status-import/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "import",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"importGet": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "GET",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status-import/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "import",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"imports": {
"Properties": {
"ParentId": {
"Ref": "Jobs",
},
"PathPart": "imports",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"importsList": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[InternalServiceError].*",
"StatusCode": 500,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[BadRequest].*",
"StatusCode": 400,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[Conflict].*",
"StatusCode": 409,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[NotFound].*",
"StatusCode": 404,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*Exception.*",
"StatusCode": 405,
},
],
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set ($root="https://\${!context.domainName}/\${!context.stage}")
{
"bucket":"\${ContentDesignerOutputBucket}",
"prefix":"status-import/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"imports",
"root":"$root"
}
",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"S3ListLambda",
"Arn",
],
},
"/invocations",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.date": true,
},
"StatusCode": 200,
},
{
"StatusCode": 404,
},
{
"StatusCode": 405,
},
{
"StatusCode": 500,
},
],
"RequestParameters": {
"method.request.querystring.perpage": false,
"method.request.querystring.token": false,
},
"ResourceId": {
"Ref": "imports",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"photo": {
"Properties": {
"ParentId": {
"Ref": "photos",
},
"PathPart": "{proxy+}",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"photoGet": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "GET",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Not Found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "AssetBucket",
},
"/examples/photos/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "photo",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"photos": {
"Properties": {
"ParentId": {
"Ref": "Examples",
},
"PathPart": "photos",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"photosList": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[InternalServiceError].*",
"StatusCode": 500,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[BadRequest].*",
"StatusCode": 400,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[Conflict].*",
"StatusCode": 409,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[NotFound].*",
"StatusCode": 404,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*Exception.*",
"StatusCode": 405,
},
],
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set ($root="https://\${!context.domainName}/\${!context.stage}")
{
"bucket":"\${AssetBucket}",
"prefix":"examples/photos/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"root":"$root"
}
",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"ExampleS3ListPhotoLambda",
"Arn",
],
},
"/invocations",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.date": true,
},
"StatusCode": 200,
},
{
"StatusCode": 404,
},
{
"StatusCode": 405,
},
{
"StatusCode": 500,
},
],
"RequestParameters": {
"method.request.querystring.perpage": false,
"method.request.querystring.token": false,
},
"ResourceId": {
"Ref": "photos",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"rootGet": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W59",
"reason": "This ApiGateway Method does not need authorization setup",
},
],
},
},
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "GET",
"Integration": {
"IntegrationResponses": [
{
"ResponseTemplates": {
"application/json": {
"Fn::Sub": "#set ($root="https://\${!context.domainName}/\${!context.stage}")
{
"region":"\${!stageVariables.Region}",
"Version":"\${InfoVar.Version}",
"BuildDate":"\${InfoVar.BuildDateString}",
"BotName":"Use LexV2 bot",
"BotVersion":"$LATEST",
"v2BotId": "\${LexV2Bot.botId}",
"v2BotAliasId": "\${LexV2Bot.botAliasId}",
"v2BotLocaleId": "\${LexV2BotLocaleIds}",
"PoolId":"\${IdPool}",
"StackName":"\${AWS::StackName}",
"ClientIdClient":"\${ClientClient}",
"ClientIdDesigner":"\${ClientDesigner}",
"UserPool":"\${UserPool}",
"StreamingWebSocketEndpoint": "$stageVariables.StreamingWebSocketEndpoint",
"SolutionHelper": "\${SolutionHelper}",
"SettingsTable": "\${SettingsTable}",
"Id":"$stageVariables.Id",
"_links":{
"root":{
"href":"$root"
},
"questions":{
"href":"$root/questions"
},
"crawler":{
"href":"$root/crawler"
},
"crawlerV2":{
"href":"$root/kendranativecrawler"
},
"bot":{
"href":"$root/bot"
},
"jobs":{
"href":"$root/jobs"
},
"connect":{
"href":"$root/connect"
},
"genesys":{
"href":"$root/genesys"
},
"translate":{
"href":"$root/translate"
},
"examples":{
"href":"$root/examples/documents"
},
"DesignerLogin":{
"href":"$stageVariables.DesignerLoginUrl"
},
"ClientLogin":{
"href":"$stageVariables.ClientLoginUrl"
},
"CognitoEndpoint":{
"href":"$stageVariables.CognitoEndpoint"
},
"Services":{
"href":"$root/services"
},
"OpenSearchDashboards":{
"href":"https://\${Urls.OpenSearchDashboards}"
}
}
}
",
},
},
"StatusCode": "200",
},
],
"RequestTemplates": {
"application/json": "{"statusCode": 200}",
},
"Type": "MOCK",
},
"MethodResponses": [
{
"StatusCode": 200,
},
],
"ResourceId": {
"Fn::GetAtt": [
"API",
"RootResourceId",
],
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"testall": {
"Properties": {
"ParentId": {
"Ref": "testalls",
},
"PathPart": "{proxy+}",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"testallDelete": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "DELETE",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "DELETE",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status-testall/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "testall",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"testallGet": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "GET",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "ContentDesignerOutputBucket",
},
"/status-testall/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "testall",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"testallPut": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "PUT",
"Integration": {
"Credentials": {
"Fn::GetAtt": [
"S3AccessRole",
"Arn",
],
},
"IntegrationHttpMethod": "PUT",
"IntegrationResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": "integration.response.header.Content-Type",
},
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/xml": "{"error":"Job not found"}",
},
"SelectionPattern": "403",
"StatusCode": 404,
},
],
"RequestParameters": {
"integration.request.path.proxy": "method.request.path.proxy",
},
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set($inputRoot = $input.path('$'))
{
"bucket":"\${TestAllBucket}",
"index":"\${Var.QnaIndex}",
"id":"$input.params('proxy')",
"config":"status-testall/$input.params('proxy')",
"tmp":"tmp-testall/$input.params('proxy')",
"key":"data-testall/$input.params('proxy')",
"filter":"$inputRoot.get('filter')",
"token":"$inputRoot.get('token')",
"locale":"$inputRoot.get('locale')",
"status":"Started"
}
",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":s3:path/",
{
"Ref": "TestAllBucket",
},
"/status-testall/{proxy}",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.content-type": false,
},
"StatusCode": 200,
},
{
"StatusCode": 400,
},
{
"StatusCode": 404,
},
],
"RequestParameters": {
"method.request.path.proxy": false,
},
"ResourceId": {
"Ref": "testall",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
"testalls": {
"Properties": {
"ParentId": {
"Ref": "Jobs",
},
"PathPart": "testall",
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Resource",
},
"testallsList": {
"Properties": {
"AuthorizationType": "AWS_IAM",
"HttpMethod": "GET",
"Integration": {
"IntegrationHttpMethod": "POST",
"IntegrationResponses": [
{
"StatusCode": 200,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[InternalServiceError].*",
"StatusCode": 500,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[BadRequest].*",
"StatusCode": 400,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[Conflict].*",
"StatusCode": 409,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*[NotFound].*",
"StatusCode": 404,
},
{
"ResponseTemplates": {
"application/json": "#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
",
},
"SelectionPattern": ".*Exception.*",
"StatusCode": 405,
},
],
"RequestTemplates": {
"application/json": {
"Fn::Sub": "#set ($root="https://\${!context.domainName}/\${!context.stage}")
{
"bucket":"\${ContentDesignerOutputBucket}",
"prefix":"status-testall/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"testall",
"root":"$root"
}
",
},
},
"Type": "AWS",
"Uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{
"Ref": "AWS::Region",
},
":lambda:path/2015-03-31/functions/",
{
"Fn::GetAtt": [
"S3ListLambda",
"Arn",
],
},
"/invocations",
],
],
},
},
"MethodResponses": [
{
"ResponseParameters": {
"method.response.header.date": true,
},
"StatusCode": 200,
},
{
"StatusCode": 404,
},
{
"StatusCode": 405,
},
{
"StatusCode": 500,
},
],
"RequestParameters": {
"method.request.querystring.perpage": false,
"method.request.querystring.token": false,
},
"ResourceId": {
"Ref": "testalls",
},
"RestApiId": {
"Ref": "API",
},
},
"Type": "AWS::ApiGateway::Method",
},
},
"Rules": {
"RequireLambdaArnForLambdaEmbeddingsApi": {
"Assertions": [
{
"Assert": {
"Fn::Not": [
{
"Fn::Equals": [
{
"Ref": "EmbeddingsLambdaArn",
},
"",
],
},
],
},
"AssertDescription": "EmbeddingsLambdaArn is required when EmbeddingsApi is set to LAMBDA.",
},
],
"RuleCondition": {
"Fn::Equals": [
{
"Ref": "EmbeddingsApi",
},
"LAMBDA",
],
},
},
},
}
`;
================================================
FILE: source/templates/master/assets.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../util');
module.exports = {
AssetBucket: {
Type: 'AWS::S3::Bucket',
DependsOn : ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
VersioningConfiguration: {
Status: 'Enabled',
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: {"Fn::Join": ["", [{Ref: 'MainAccessLogBucket'},"/Assets/"]]},
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
Metadata: {
cfn_nag: {
rules_to_suppress: [{
id: 'F14',
reason: 'AccessControl is deprecated.',
}],
},
guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL'),
},
},
HTTPSOnlyAssetBucketPolicy: util.httpsOnlyBucketPolicy('AssetBucket'),
AssetClean: {
Type: 'Custom::S3Clean',
DependsOn: ['CFNInvokePolicy', 'HTTPSOnlyAssetBucketPolicy'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
Bucket: { Ref: 'AssetBucket' },
},
},
AssetZipVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: {
'Fn::Join': ['', [
{ Ref: 'BootstrapPrefix' },
'/assets.zip',
]],
},
BuildDate: (new Date()).toISOString(),
},
},
AssetUnzip: {
Type: 'Custom::S3Unzip',
DependsOn: ['AssetClean'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
SrcBucket: { Ref: 'BootstrapBucket' },
Key: {
'Fn::Join': ['', [
{ Ref: 'BootstrapPrefix' },
'/assets.zip',
]],
},
DstBucket: { Ref: 'AssetBucket' },
version: { Ref: 'AssetZipVersion' },
},
},
};
================================================
FILE: source/templates/master/bucket.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../util');
module.exports = {
MainAccessLogBucket: {
Type: 'AWS::S3::Bucket',
Properties: {
VersioningConfiguration: {
Status: 'Enabled',
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
UpdateReplacePolicy: 'Retain',
// retain only policy is for security auditing purposes
DeletionPolicy: 'Retain',
Metadata: {
cfn_nag: util.cfnNag(['W35']),
guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL'),
},
},
MainAccessLogsBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
DependsOn: 'MainAccessLogBucket',
Properties: {
Bucket: {
Ref: 'MainAccessLogBucket',
},
PolicyDocument: {
Statement: [
{
Action: 's3:PutObject',
Condition: {
ArnLike: {
'aws:SourceArn': 'arn:aws:s3:::*',
},
Bool: {
'aws:SecureTransport': 'true',
},
StringEquals: {
'aws:SourceAccount': { Ref: 'AWS::AccountId' },
},
},
Effect: 'Allow',
Principal: {
Service: 'logging.s3.amazonaws.com',
},
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'MainAccessLogBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'MainAccessLogBucket',
'Arn',
],
},
],
],
},
],
Sid: 'S3ServerAccessLogsPolicy',
},
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'MainAccessLogBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'MainAccessLogBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
ExportBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn: ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
LifecycleConfiguration: {
Rules: [{
NoncurrentVersionExpirationInDays: 1,
Status: 'Enabled',
}, {
AbortIncompleteMultipartUpload: {
DaysAfterInitiation: 1,
},
Status: 'Enabled',
}],
},
VersioningConfiguration: {
Status: 'Enabled',
},
CorsConfiguration: {
CorsRules: [{
AllowedHeaders: ['*'],
AllowedMethods: ['GET'],
AllowedOrigins: ['*'],
}],
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: { 'Fn::Join': ['', [{ Ref: 'MainAccessLogBucket' }, '/Export/']] },
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
UpdateReplacePolicy: 'Retain',
DeletionPolicy: 'Retain',
},
HTTPSOnlyExportBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'ExportBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ExportBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ExportBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
ImportBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn: ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
LifecycleConfiguration: {
Rules: [{
ExpirationInDays: 1,
Status: 'Enabled',
}],
},
VersioningConfiguration: {
Status: 'Enabled',
},
CorsConfiguration: {
CorsRules: [{
AllowedHeaders: ['*'],
AllowedMethods: ['PUT'],
AllowedOrigins: ['*'],
}],
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: { 'Fn::Join': ['', [{ Ref: 'MainAccessLogBucket' }, '/Import/']] },
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
},
HTTPSOnlyImportBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'ImportBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ImportBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ImportBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
TestAllBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn: ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
LifecycleConfiguration: {
Rules: [{
ExpirationInDays: 1,
Status: 'Enabled',
}],
},
VersioningConfiguration: {
Status: 'Enabled',
},
CorsConfiguration: {
CorsRules: [{
AllowedHeaders: ['*'],
AllowedMethods: ['GET'],
AllowedOrigins: ['*'],
}],
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: { 'Fn::Join': ['', [{ Ref: 'MainAccessLogBucket' }, '/TestAll/']] },
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
},
HTTPSOnlyTestAllBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'TestAllBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'TestAllBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'TestAllBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
ContentDesignerOutputBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn: ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
LifecycleConfiguration: {
Rules: [{
ExpirationInDays: 1,
Status: 'Enabled',
}],
},
VersioningConfiguration: {
Status: 'Enabled',
},
CorsConfiguration: {
CorsRules: [{
AllowedHeaders: ['*'],
AllowedMethods: ['GET'],
AllowedOrigins: ['*'],
}],
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: { 'Fn::Join': ['', [{ Ref: 'MainAccessLogBucket' }, '/ContentDesignerOutput/']] },
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
},
},
HTTPSOnlyContentDesignerOutputBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'ContentDesignerOutputBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ContentDesignerOutputBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ContentDesignerOutputBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
ContentDesignerOutputClean: {
Type: 'Custom::S3Clean',
DependsOn: [
'CFNInvokePolicy',
],
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'S3Clean',
'Arn',
],
},
Bucket: { Ref: 'ContentDesignerOutputBucket' },
},
},
};
================================================
FILE: source/templates/master/cfn/__tests__/handler.fixtures.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.event = {
RequestType: 'Create',
ResponseURL: 'https://localhost',
ResourceProperties: {
Bucket: 'test-bucket',
Key: 'test-key',
},
};
exports.endMock = jest.fn();
exports.writeMock = jest.fn().mockImplementation((body) => {
expect(JSON.parse(body).PhysicalResourceId).toEqual('mock log stream name');
});
exports.doneMock = jest.fn();
================================================
FILE: source/templates/master/cfn/__tests__/handler.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require("aws-sdk-client-mock-jest");
const { EventEmitter } = require('events');
const httpsMock = require('https');
const Stream = require('stream');
const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
const { handler } = require('../handler');
const { event, endMock, writeMock, doneMock } = require('./handler.fixtures');
const context = {
logStreamName: 'mock log stream name',
done: doneMock,
};
const emitter = new EventEmitter();
emitter.write = writeMock;
emitter.end = endMock;
describe('bootstrap handler', () => {
beforeEach(() => {
jest.resetModules();
endMock.mockRestore();
doneMock.mockRestore();
writeMock.mockRestore();
s3ClientMock.reset();
});
it('should send a put request to the provided url', async () => {
const message = new Stream();
s3ClientMock.on(HeadObjectCommand)
.resolvesOnce({
VersionId: 4,
});
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
expect(s3ClientMock).toHaveReceivedCommandWith(HeadObjectCommand, { Bucket: "test-bucket", Key: 'test-key' });
expect(writeMock).toHaveBeenCalledWith("{\"Status\":\"SUCCESS\",\"Reason\":\"See the details in CloudWatch Log Stream: mock log stream name\",\"PhysicalResourceId\":\"mock log stream name\",\"NoEcho\":false,\"Data\":{\"version\":4}}");
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should set version id to 1 if falsy', async () => {
const message = new Stream();
s3ClientMock.on(HeadObjectCommand)
.resolvesOnce({});
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
expect(s3ClientMock).toHaveReceivedCommandWith(HeadObjectCommand, { Bucket: "test-bucket", Key: 'test-key' });
expect(writeMock).toHaveBeenCalledWith("{\"Status\":\"SUCCESS\",\"Reason\":\"See the details in CloudWatch Log Stream: mock log stream name\",\"PhysicalResourceId\":\"mock log stream name\",\"NoEcho\":false,\"Data\":{\"version\":1}}");
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should close context on error', async () => {
const message = new Stream();
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
emitter.emit('error', 'error message');
expect(writeMock).toHaveBeenCalled();
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should respond to delete requests', async () => {
const message = new Stream();
const clonedEvent = JSON.parse(JSON.stringify(event));
clonedEvent.RequestType = 'Delete';
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(clonedEvent, context);
emitter.emit('end', 'end');
expect(writeMock).toHaveBeenCalled();
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
});
================================================
FILE: source/templates/master/cfn/handler.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3');
const region = process.env.AWS_REGION;
const client = new S3Client({
customUserAgent: [
[`AWSSOLUTION/${process.env.SOLUTION_ID}/${process.env.SOLUTION_VERSION}`],
[`AWSSOLUTION-CAPABILITY/${process.env.SOLUTION_ID}-C023/${process.env.SOLUTION_VERSION}`],
],
region,
});
const SUCCESS = 'SUCCESS';
const FAILED = 'FAILED';
const https = require('https');
const { URL } = require('url');
async function send(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
return new Promise((resolve, reject) => {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData,
});
console.log('Response body:\n', responseBody);
const parsedUrl = new URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname + parsedUrl.search,
method: 'PUT',
headers: {
'content-type': '',
'content-length': responseBody.length,
},
};
const request = https.request(options, (response) => {
console.log(`Status code: ${response.statusCode}`);
console.log(`Status message: ${response.statusMessage}`);
response.on('end', () => {
resolve();
});
});
request.on('error', (error) => {
console.log(`send(..) failed executing https.request(..): ${error}`);
reject(error);
});
request.write(responseBody);
request.end();
});
}
exports.handler = async function (event, context) {
console.log(JSON.stringify(event, null, 2));
if (event.RequestType !== 'Delete') {
const params = {
Bucket: event.ResourceProperties.Bucket,
Key: event.ResourceProperties.Key,
};
const headObjCmd = new HeadObjectCommand(params);
try {
const result = await client.send(headObjCmd);
await send(event, context, SUCCESS, {
version: result.VersionId ? result.VersionId : 1,
});
} catch (e) {
console.log(e);
await send(event, context, FAILED);
}
} else {
await send(event, context, SUCCESS);
}
context.done();
};
================================================
FILE: source/templates/master/cfn/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
VersionLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-VersionLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
VersionLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
// join files by new line to ensure valid javascript
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf-8'),
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'VersionLambdaLogGroup' },
},
MemorySize: '3008',
Role: { 'Fn::GetAtt': ['CFNLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 60,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'CustomResource',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
CFNVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['VersionLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/cfn.zip' },
BuildDate: (new Date()).toISOString(),
},
},
CFNLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-CFNLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
CFNLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: {
'Fn::Join': ['', [
{ Ref: 'BootstrapPrefix' },
'/lambda/cfn.zip',
]],
},
S3ObjectVersion: { 'Fn::GetAtt': ['CFNVersion', 'version'] },
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables(),
}
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'CFNLambdaLogGroup' },
},
MemorySize: '3008',
Role: { 'Fn::GetAtt': ['CFNLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 180,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'CustomResource',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
CFNInvokePolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
{ 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
],
}],
},
Roles: [{ Ref: 'CFNLambdaRole' }],
},
},
};
================================================
FILE: source/templates/master/cognito/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
CognitoDomain: {
Type: 'Custom::CognitoDomain',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
UserPool: { Ref: 'UserPool' },
},
},
CognitoLoginClient: {
Type: 'Custom::CognitoLogin',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
UserPool: { Ref: 'UserPool' },
ClientId: { Ref: 'ClientClient' },
LoginCallbackUrls: [
{ 'Fn::GetAtt': ['Urls', 'Client'] },
],
CSS: require('./style').client,
},
},
CognitoLoginDesigner: {
Type: 'Custom::CognitoLogin',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
UserPool: { Ref: 'UserPool' },
ClientId: { Ref: 'ClientDesigner' },
LoginCallbackUrls: [
{ 'Fn::GetAtt': ['Urls', 'Designer'] },
],
LogoutCallbackUrls: [
{ 'Fn::GetAtt': ['Urls', 'Designer'] },
],
CSS: require('./style').designer,
},
},
DesignerLogin: {
Type: 'Custom::CognitoUrl',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
adad: 'adaad',
ClientId: { Ref: 'ClientDesigner' },
Domain: { Ref: 'CognitoDomain' },
LoginRedirectUrl: { 'Fn::GetAtt': ['Urls', 'Designer'] },
response_type: 'code',
},
},
ClientLogin: {
Type: 'Custom::CognitoUrl',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
ClientId: { Ref: 'ClientClient' },
Domain: { Ref: 'CognitoDomain' },
LoginRedirectUrl: { 'Fn::GetAtt': ['Urls', 'Client'] },
response_type: 'code',
},
},
User: {
Type: 'AWS::Cognito::UserPoolUser',
DependsOn: ['SignupPermision', 'MessagePermision', 'OpenSearchDashboardsRoleAttachment', 'RoleAttachment'],
Properties: {
DesiredDeliveryMediums: ['EMAIL'],
UserAttributes: [
{
Name: 'email',
Value: { Ref: 'Email' },
},
],
Username: { Ref: 'Username' },
UserPoolId: { Ref: 'UserPool' },
},
},
UserToGroup: {
Type: 'AWS::Cognito::UserPoolUserToGroupAttachment',
Properties: {
GroupName: { Ref: 'Admins' },
Username: { Ref: 'User' },
UserPoolId: { Ref: 'UserPool' },
},
},
IdPool: {
Type: 'AWS::Cognito::IdentityPool',
Properties: {
IdentityPoolName: { 'Fn::Join': ['-', ['QnaBotIdPool', { Ref: 'AWS::StackName' }]] },
AllowUnauthenticatedIdentities: true,
CognitoIdentityProviders: [{
ClientId: { Ref: 'ClientDesigner' },
ProviderName: { 'Fn::GetAtt': ['UserPool', 'ProviderName'] },
ServerSideTokenCheck: true,
}, {
ClientId: { Ref: 'ClientClient' },
ProviderName: { 'Fn::GetAtt': ['UserPool', 'ProviderName'] },
ServerSideTokenCheck: true,
},
],
},
Metadata: { cfn_nag: util.cfnNag(['W57']) },
},
OpenSearchDashboardsIdPool: {
Type: 'AWS::Cognito::IdentityPool',
Properties: {
IdentityPoolName: { 'Fn::Join': ['-', ['OpenSearchDashboardsIdPool', { Ref: 'AWS::StackName' }]] },
AllowUnauthenticatedIdentities: false,
},
},
OpenSearchDashboardsRoleAttachment: {
Type: 'Custom::CognitoRole',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
IdentityPoolId: { Ref: 'OpenSearchDashboardsIdPool' },
DomainName: { 'Fn::GetAtt': ['ESVar', 'ESDomain'] },
Roles: {
authenticated: { 'Fn::GetAtt': ['UserRole', 'Arn'] },
unauthenticated: { 'Fn::GetAtt': ['UnauthenticatedRole', 'Arn'] },
},
RoleMappings: [{
ClientId: { 'Fn::GetAtt': ['OpenSearchDashboardsClient', 'ClientId'] },
UserPool: { Ref: 'UserPool' },
Type: 'Rules',
AmbiguousRoleResolution: 'Deny',
RulesConfiguration: {
Rules: [{
Claim: 'cognito:groups',
MatchType: 'Contains',
Value: 'Admin',
RoleARN: { 'Fn::GetAtt': ['OpenSearchDashboardsRole', 'Arn'] },
}],
},
}],
},
},
RoleAttachment: {
Type: 'Custom::CognitoRole',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
IdentityPoolId: { Ref: 'IdPool' },
Roles: {
authenticated: { 'Fn::GetAtt': ['UserRole', 'Arn'] },
unauthenticated: { 'Fn::GetAtt': ['UnauthenticatedRole', 'Arn'] },
},
RoleMappings: [{
ClientId: { Ref: 'ClientClient' },
UserPool: { Ref: 'UserPool' },
Type: 'Rules',
AmbiguousRoleResolution: 'AuthenticatedRole',
RulesConfiguration: {
Rules: [{
Claim: 'cognito:groups',
MatchType: 'Contains',
Value: 'Admin',
RoleARN: { 'Fn::GetAtt': ['UserRole', 'Arn'] },
}],
},
}, {
ClientId: { Ref: 'ClientDesigner' },
UserPool: { Ref: 'UserPool' },
Type: 'Rules',
AmbiguousRoleResolution: 'Deny',
RulesConfiguration: {
Rules: [{
Claim: 'cognito:groups',
MatchType: 'Contains',
Value: 'Admin',
RoleARN: { 'Fn::GetAtt': ['AdminRole', 'Arn'] },
}],
},
}],
},
},
UserPool: {
Type: 'AWS::Cognito::UserPool',
Properties: {
UserPoolName: { 'Fn::Join': ['-', ['UserPool', { Ref: 'AWS::StackName' }]] },
AdminCreateUserConfig: {
AllowAdminCreateUserOnly: { 'Fn::If': ['AdminSignUp', true, false] },
InviteMessageTemplate: {
EmailMessage: { 'Fn::Sub': fs.readFileSync(`${__dirname}/invite.txt`, 'utf8') },
EmailSubject: 'Welcome to QnABot!',
},
},
AliasAttributes: ['email'],
AutoVerifiedAttributes: ['email'],
Schema: [{
Required: true,
Name: 'email',
AttributeDataType: 'String',
Mutable: true,
}],
LambdaConfig: {
CustomMessage: { 'Fn::GetAtt': ['MessageLambda', 'Arn'] },
PreSignUp: { 'Fn::GetAtt': ['SignupLambda', 'Arn'] },
},
},
},
OpenSearchDashboardsClient: {
Type: 'Custom::ESCognitoClient',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
UserPool: { Ref: 'UserPool' },
DomainName: { 'Fn::GetAtt': ['ESVar', 'ESDomain'] },
},
},
ClientDesigner: {
Type: 'AWS::Cognito::UserPoolClient',
Properties: {
ClientName: {
'Fn::Join': ['-', [
'UserPool',
{ Ref: 'AWS::StackName' },
'designer',
]],
},
GenerateSecret: false,
UserPoolId: { Ref: 'UserPool' },
},
},
ClientClient: {
Type: 'AWS::Cognito::UserPoolClient',
Properties: {
ClientName: {
'Fn::Join': ['-', [
'UserPool',
{ Ref: 'AWS::StackName' },
'client',
]],
},
GenerateSecret: false,
UserPoolId: { Ref: 'UserPool' },
},
},
Users: {
Type: 'AWS::Cognito::UserPoolGroup',
Properties: {
GroupName: 'Users',
UserPoolId: { Ref: 'UserPool' },
},
},
Admins: {
Type: 'AWS::Cognito::UserPoolGroup',
Properties: {
GroupName: 'Admins',
UserPoolId: { Ref: 'UserPool' },
},
},
};
================================================
FILE: source/templates/master/cognito/invite.txt
================================================
Hello {username},
Welcome to QnABot! Your temporary password is:
{####}
When the CloudFormation stack is COMPLETE, use the link below to log in to QnABot Content Designer, set your permanent password, and start building your bot!
${ApiUrl.Name}/pages/designer
Good luck!
QnABot (www.amazon.com/qnabot)
================================================
FILE: source/templates/master/cognito/style/README.md
================================================
# Cognito Login Style
style for cognito hosted login
================================================
FILE: source/templates/master/cognito/style/client.scss
================================================
.logo-customizable {
max-width: 60%;
max-height: 30%;
}
.banner-customizable {
padding: 25px 0px 25px 0px;
background-color: lightgray;
}
.label-customizable {
font-weight: 410;
}
.textDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.idpDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.legalText-customizable {
color: #747474;
font-size: 11px;
}
.submitButton-customizable {
font-size: 14px;
font-weight: bold;
margin: 20px 0px 10px 0px;
height: 40px;
width: 100%;
color: #fff;
background-color: #337ab7;
}
.submitButton-customizable:hover {
color: #fff;
background-color: #286090;
}
.errorMessage-customizable {
padding: 5px;
font-size: 14px;
width: 100%;
background: #F5F5F5;
border: 2px solid #D64958;
color: #D64958;
}
.inputField-customizable {
width: 100%;
height: 34px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
}
.inputField-customizable:focus {
border-color: #66afe9;
outline: 0;
}
.idpButton-customizable {
height: 41px;
width: 100%;
text-align: center;
margin-bottom: 15px;
color: #fff;
background-color: #5bc0de;
border-color: #46b8da;
}
.idpButton-customizable:hover {
color: #fff;
background-color: #31b0d5;
}
.socialButton-customizable {
height: 40px;
text-align: left;
width: 100%;
margin-bottom: 15px;
}
.redirect-customizable {
text-align: center;
}
.passwordCheck-notValid-customizable {
color: #DF3312;
}
.passwordCheck-valid-customizable {
color: #19BF00;
}
.background-customizable {
background-color: #fff;
}
================================================
FILE: source/templates/master/cognito/style/cognito-login.css
================================================
body {
font-family: "Arial", serif;
color: #3B3B3B;
background: #909090;
}
@media (min-width: 768px) {
.modal-dialog {
width: 700px;
}
}
.panel {
padding-top: 15px;
padding-bottom: 15px;
padding-left: 15px;
padding-right: 15px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0);
border-radius: 0;
background: transparent;
}
.modal-content {
width: 100% !important;
margin: 0px auto;
border: 0px;
overflow: hidden;
border-radius: 0%;
}
.modal-content-mobile {
max-width: 350px;
}
.modal-content-desktop {
min-width: 700px;
}
.login-or {
position: relative;
font-size: 16px;
padding-top: 7px;
padding-bottom: 7px;
margin-top: 8px;
}
.hr-or {
color: #cdcdcd;
background-color: #cdcdcd;
height: 1px;
margin-top: 0px !important;
margin-bottom: 0px !important;
}
.panel-left-border {
border-left-style: solid;
border-left-color: #F5F5F5;
border-left-width: 2px;
}
.panel-right-border {
border-right-style: solid;
border-right-color: #F5F5F5;
border-right-width: 2px;
margin-right: -2px;
}
.span-or {
display: block;
position: absolute;
left: 50%;
top: -2px;
margin-left: -25px;
background-color: #fff;
text-align: center;
padding: 0px 5px 0px 5px;
color: #A3A3A3;
}
.span-or-verical {
font-size: 16px;
display: block;
position: absolute;
top: 40%;
margin-left: -25px;
background-color: #fff;
text-align: center;
color: #A3A3A3;
padding-top: 15px;
padding-bottom: 15px;
}
.facebook-button {
background-color: #4267b2;
color: white;
}
.facebook-button:hover {
background-color: #2955ad;
color: white;
}
.google-button {
background-color: #d3d3d3;
color: black;
}
.google-button:hover {
background-color: #c4c4c4;
}
.amazon-button {
background-color: #f7bc22;
color: black;
}
.amazon-button:hover {
background-color: #ffae00;
color: black;
}
.logo-customizable {
max-width: 60%;
max-height: 30%;
}
.banner-customizable {
padding: 25px 0px 25px 0px;
background-color: lightgray;
}
.label-customizable {
font-weight: 400;
}
.textDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.idpDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.legalText-customizable {
color: #747474;
font-size: 11px;
}
.submitButton-customizable {
font-size: 14px;
font-weight: bold;
margin: 20px 0px 10px 0px;
height: 40px;
width: 100%;
color: #fff;
background-color: #337ab7;
border-color: #2e6da4
}
.submitButton-customizable:hover {
color: #fff;
background-color: #286090;
border-color: #204d74
}
.errorMessage-customizable {
padding: 5px;
font-size: 14px;
width: 100%;
background: #F5F5F5;
border: 2px solid #D64958;
color: #D64958;
}
.inputField-customizable {
width: 100%;
height: 34px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
}
.inputField-customizable:focus {
border-color: #66afe9;
outline: 0;
}
.idpButton-customizable {
height: 40px;
width: 100%;
text-align: center;
margin-bottom: 15px;
color: #fff;
background-color: #5bc0de;
border-color: #46b8da;
}
.idpButton-customizable:hover {
color: #fff;
background-color: #31b0d5;
border-color: #269abc
}
.socialButton-customizable {
height: 40px;
text-align: left;
width: 100%;
margin-bottom: 15px;
}
.redirect-customizable {
text-align: center;
}
.passwordCheck-notValid-customizable {
color: #DF3312;
}
.passwordCheck-valid-customizable {
color: #19BF00;
}
.background-customizable {
background-color: #fff;
}
.social-logo{
width:35px;
height:100%;
text-align:center;
padding-right: 10px;
display: inline-block;
vertical-align: middle;
}
================================================
FILE: source/templates/master/cognito/style/designer.scss
================================================
.logo-customizable {
max-width: 60%;
max-height: 30%;
}
.banner-customizable {
padding: 25px 0px 25px 0px;
background-color: lightgray;
margin-left: auto;
margin-right: auto;
}
.label-customizable {
font-weight: 410;
}
.textDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.idpDescription-customizable {
padding-top: 10px;
padding-bottom: 10px;
display: block;
font-size: 16px;
}
.legalText-customizable {
color: #747474;
font-size: 11px;
}
.submitButton-customizable {
font-size: 14px;
font-weight: bold;
margin: 20px 0px 10px 0px;
height: 40px;
width: 100%;
color: #fff;
background-color: #337ab7;
}
.submitButton-customizable:hover {
color: #fff;
background-color: #286090;
}
.errorMessage-customizable {
padding: 5px;
font-size: 14px;
width: 100%;
background: #F5F5F5;
border: 2px solid #D64958;
color: #D64958;
}
.inputField-customizable {
width: 100%;
height: 34px;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
}
.inputField-customizable:focus {
border-color: #66afe9;
outline: 0;
}
.idpButton-customizable {
height: 41px;
width: 100%;
text-align: center;
margin-bottom: 15px;
color: #fff;
background-color: #5bc0de;
border-color: #46b8da;
}
.idpButton-customizable:hover {
color: #fff;
background-color: #31b0d5;
}
.socialButton-customizable {
height: 40px;
text-align: left;
width: 100%;
margin-bottom: 15px;
}
.redirect-customizable {
text-align: center;
}
.passwordCheck-notValid-customizable {
color: #DF3312;
}
.passwordCheck-valid-customizable {
color: #19BF00;
}
.background-customizable {
background-color: #fff;
}
================================================
FILE: source/templates/master/cognito/style/index.html
================================================
Signin
Sign in with your username and password
Sign in with your username and password
================================================
FILE: source/templates/master/cognito/style/index.js
================================================
#! /usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const sass = require('sass');
const client = sass.compileString(
fs.readFileSync(`${__dirname}/client.scss`, 'utf8'),
{ style: 'compressed' },
).css;
const designer = sass.compileString(
fs.readFileSync(`${__dirname}/designer.scss`, 'utf8'),
{ style: 'compressed' },
).css;
module.exports = {
client,
designer,
};
================================================
FILE: source/templates/master/config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const methods = [];
_.forEach(require('./routes'), (value, key) => {
value.Type === 'AWS::ApiGateway::Method' ? methods.push(key) : null; // NOSONAR used iterative expression
});
const permissions = _.keys(require('./lambda'))
.filter((x) => x.match(/^InvokePermission/))
.filter((x) => ![
'InvokePermissionLexBuildLambda',
'InvokePermissionLexBuildLambdaPoll',
'InvokePermissionLexStatusLambda',
].includes(x));
const util = require('../util');
module.exports = {
API: {
Type: 'AWS::ApiGateway::RestApi',
Properties: {
Name: { Ref: 'AWS::StackName' },
Description: 'An Api interface for the admin actions on the QNA bot',
BinaryMediaTypes: ['image/png', 'font/woff', 'font/woff2'],
MinimumCompressionSize: 500000,
},
},
Deployment: {
Type: 'Custom::ApiDeployment',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
restApiId: { Ref: 'API' },
buildDate: new Date(),
stage: 'prod',
LexV2BotLocaleIds: { Ref: 'LexV2BotLocaleIds' },
},
DependsOn: methods.concat(permissions),
},
Stage: stage('prod'),
ApiGatewayAccount: {
Type: 'AWS::ApiGateway::Account',
Properties: {
CloudWatchRoleArn: {
'Fn::GetAtt': ['ApiGatewayCloudWatchLogsRole', 'Arn'],
},
},
},
DocumentationVersion: {
Type: 'AWS::ApiGateway::DocumentationVersion',
DependsOn: ['BotDoc'],
Properties: {
Description: '',
DocumentationVersion: '1.0',
RestApiId: { Ref: 'API' },
},
},
};
function stage(name) {
return {
Type: 'AWS::ApiGateway::Stage',
Properties: {
DeploymentId: {
Ref: 'Deployment',
},
RestApiId: {
Ref: 'API',
},
StageName: name,
MethodSettings: [{
CacheDataEncrypted: true,
CachingEnabled: true,
DataTraceEnabled: false,
HttpMethod: '*',
LoggingLevel: 'INFO',
ResourcePath: '/*',
}],
Variables: {
Id: 'QnABot',
Region: { Ref: 'AWS::Region' },
CognitoEndpoint: { 'Fn::GetAtt': ['DesignerLogin', 'Domain'] },
DesignerLoginUrl: {
'Fn::Join': ['', [
{ 'Fn::GetAtt': ['ApiUrl', 'Name'] },
'/pages/designer',
]],
},
ClientLoginUrl: {
'Fn::If': [
'Public',
{ 'Fn::GetAtt': ['Urls', 'Client'] },
{
'Fn::Join': ['', [
{ 'Fn::GetAtt': ['ApiUrl', 'Name'] },
'/pages/client',
]],
},
],
},
StreamingWebSocketEndpoint: {
'Fn::If': [
'StreamingEnabled',
{ 'Fn::GetAtt': ['StreamingStack', 'Outputs.StreamingWebSocketEndpoint'] },
''
]
},
},
},
Metadata: { cfn_nag: util.cfnNag(['W64', 'W69']) },
};
}
================================================
FILE: source/templates/master/dashboard/README.md
================================================
# CloudWatch Dashboard Template
template for cloudwatch dashboard
================================================
FILE: source/templates/master/dashboard/body.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const lambdas = require('./lambdas');
const opensearch = require('./opensearch');
const util = require('./util');
let widgets = [util.Title('# QnABot:${AWS::StackName} Dashboard', 0)];
widgets = widgets.concat(opensearch(util.yOffset(widgets)));
widgets = widgets.concat(lambdas(util.yOffset(widgets)));
module.exports = { widgets };
================================================
FILE: source/templates/master/dashboard/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
dashboard: {
Type: 'AWS::CloudWatch::Dashboard',
Properties: {
DashboardName: { 'Fn::Sub': '${AWS::Region}-${AWS::StackName}' },
DashboardBody: { 'Fn::Sub': JSON.stringify(require('./body')) },
},
},
};
================================================
FILE: source/templates/master/dashboard/lambdas.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const util = require('./util');
const files = [
require('../UpgradeAutoExport'),
require('../assets'),
require('../bucket'),
require('../cfn'),
require('../cognito'),
require('../dynamodb'),
require('../examples'),
require('../exportstack'),
require('../importstack'),
require('../lambda-layers'),
require('../lex'),
require('../lex-build'),
require('../lexv2-build'),
require('../opensearch'),
require('../policies.json'),
require('../proxy-es'),
require('../proxy-lex'),
require('../roles.json'),
require('../routes'),
require('../s3'),
require('../s3-clean'),
require('../schemaLambda'),
require('../settings'),
require('../signup'),
require('../solution-helper'),
require('../tstallstack'),
require('../var'),
];
const lambdas = {};
_.forEach(_.assign.apply({}, files), (value, key) => {
if (value.Type === 'AWS::Lambda::Function' && key !== 'ESInfoLambda') {
const type = _.fromPairs(value.Properties.Tags.map((x) => [x.Key, x.Value])).Type;
if (!lambdas[type]) {
lambdas[type] = [];
}
lambdas[type].push(key);
}
});
module.exports = function (main_offset) {
const Lambda_title = util.Title('## Lambda Function', main_offset + 6);
const lambda_widgets = _.map(lambdas, (value, key) => ({ list: value.map(util.lambda), name: key })).reduce(
(accumulation, current) => {
const title = util.Title(`### ${current.name}`, accumulation.offset);
accumulation.offset += title.height;
accumulation.list.push(title);
current.list.map(util.place(accumulation.offset)).forEach((x) => {
accumulation.list.push(x);
});
accumulation.offset = Math.max(...accumulation.list.map((x) => x.y)) + 6;
return accumulation;
},
{ list: [], offset: main_offset + 6 + Lambda_title.height },
);
return _.flatten([Lambda_title, lambda_widgets.list]);
};
================================================
FILE: source/templates/master/dashboard/opensearch.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const util = require('./util');
module.exports = function (offset) {
const title = util.Title('## OpenSearch', offset);
const widgets = [
{
type: 'metric',
width: 6,
height: 6,
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/ES', 'ReadLatency', 'DomainName', '${ESVar.ESDomain}', 'ClientId', '${AWS::AccountId}'],
],
region: '${AWS::Region}',
},
},
{
type: 'metric',
width: 6,
height: 6,
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/ES', 'ReadIOPS', 'DomainName', '${ESVar.ESDomain}', 'ClientId', '${AWS::AccountId}'],
['.', 'ReadThroughput', '.', '.', '.', '.', { yAxis: 'right' }],
],
region: '${AWS::Region}',
},
},
{
type: 'metric',
width: 6,
height: 6,
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/ES', 'CPUUtilization', 'DomainName', '${ESVar.ESDomain}', 'ClientId', '${AWS::AccountId}'],
],
region: '${AWS::Region}',
},
},
{
type: 'metric',
x: 18,
y: 0,
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/ES', 'ClusterUsedSpace', 'DomainName', '${ESVar.ESDomain}', 'ClientId', '${AWS::AccountId}'],
['.', 'SearchableDocuments', '.', '.', '.', '.', { yAxis: 'right' }],
],
region: '${AWS::Region}',
},
},
{
type: 'metric',
width: 6,
height: 6,
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/ES', 'ClusterStatus.green', 'DomainName', '${ESVar.ESDomain}', 'ClientId', '${AWS::AccountId}', { color: '#2ca02c' }],
['.', 'ClusterStatus.red', '.', '.', '.', '.', { color: '#d62728' }],
['.', 'ClusterStatus.yellow', '.', '.', '.', '.', { color: '#bcbd22' }],
],
region: '${AWS::Region}',
},
},
].map(util.place(offset + title.height));
return _.flatten([title, widgets]);
};
================================================
FILE: source/templates/master/dashboard/util.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.yOffset = function (widgets) {
const start = Math.max(...widgets.map((x) => x.y));
const height = Math.max(...widgets.filter((x) => x.y === start).map((x) => x.height));
return start + height + 1;
};
exports.place = function (yOffset) {
return (value, index, collection) => {
value.height = 6;
value.width = 6;
value.x = (index % (24 / 6)) * 6;
value.y = (Math.floor(index / (24 / 6)) * 6) + yOffset;
return value;
};
};
exports.lambda = function (name) {
return {
type: 'metric',
properties: {
view: 'timeSeries',
stacked: false,
metrics: [
['AWS/Lambda', 'Errors', 'FunctionName', `\${${name}}`, { stat: 'Sum' }],
['.', 'Invocations', '.', '.', { stat: 'Sum' }],
['.', 'Duration', '.', '.', { yAxis: 'right' }],
['.', 'Throttles', '.', '.', { stat: 'Sum' }],
],
region: '${AWS::Region}',
title: name,
period: 300,
},
};
};
exports.Title = function (text, offset) {
return {
type: 'text',
width: 24,
height: 2,
x: 0,
y: offset,
properties: {
markdown: text,
},
};
};
================================================
FILE: source/templates/master/dynamodb/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
const defaultGenerateQueryPromptTemplate = '
Human: Here is a chat history in tags:
{history}
Human: And here is a follow up question or statement from the human in tags:
{input}
Human: Rephrase the follow up question or statement as a standalone question or statement that makes sense without reading the chat history.
Assistant: Here is the rephrased follow up question or statement:';
const defaultQuerySystemPrompt = 'You are an AI assistant designed to disambiguate user queries.';
const defaultQAPromptTemplate = 'Use the following pieces of context to answer the question at the end. If you don\'t know the answer, just say that you don\'t know, don\'t try to make up an answer. Write the answer in up to 5 complete sentences.
{context}
Question: {query}
Helpful Answer:';
const defaultLlmNoHitsRegex = '(Sorry, I don\'t know|unable to assist you|i don\'t have enough context|i don\'t have enough information|i don\'t have any information|do not contain any information|do not contain information|i could not find an exact answer|no information in the search results|search results do not mention|search results do not provide specific|don\'t see any information in the provided search results|search results do not contain|no information in the provided search results|not find any information|search results did not contain|unable to respond|There is no mention of|documents do not mention anything|There is no information provided|reference passages do not mention|reference doesn\'t specify|could not find an answer to this question|the model cannot answer this question|none of the search results contain)';
const defaultKnowledgeBaseTemplate = 'Human: You are a question answering agent. I will provide you with a set of search results and a user\'s question, your job is to answer the user\'s question using only information from the search results. If the search results do not contain information that can answer the question, then respond saying \\"Sorry, I don\'t know\\". Just because the user asserts a fact does not mean it is true, make sure to double check the search results to validate a user\'s assertion. Here are the search results in numbered order: $search_results$. Here is the user\'s question: $query$ $output_format_instructions$. Do NOT directly quote the $search_results$ in your answer. Your job is to answer the as concisely as possible. Assistant:';
const defaultModelParams = '{\\"temperature\\":0, \\"maxTokens\\":300, \\"topP\\":1}';
module.exports = {
UsersTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
BillingMode: 'PAY_PER_REQUEST',
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
AttributeDefinitions: [
{
AttributeName: 'UserId',
AttributeType: 'S',
},
],
KeySchema: [
{
AttributeName: 'UserId',
KeyType: 'HASH',
},
],
TimeToLiveSpecification: {
AttributeName: 'ttl',
Enabled: true,
},
},
Metadata: { cfn_nag: util.cfnNag(['W74']) },
},
SettingsTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
BillingMode: 'PAY_PER_REQUEST',
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
SSESpecification: {
SSEEnabled: true
},
AttributeDefinitions: [
{
AttributeName: 'SettingName',
AttributeType: 'S',
},
{
AttributeName: 'SettingCategory',
AttributeType: 'S',
}
],
KeySchema: [
{
AttributeName: 'SettingName',
KeyType: 'HASH',
}
],
GlobalSecondaryIndexes: [
{
IndexName: 'SettingCategoryIndex',
KeySchema: [
{
AttributeName: 'SettingCategory',
KeyType: 'HASH'
}
],
Projection: {
ProjectionType: 'ALL'
}
}
],
},
Metadata: { cfn_nag: util.cfnNag(['W74']) },
},
SettingsInitializer: {
Type: 'Custom::SettingsInitializer',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
SettingsTable: { Ref: 'SettingsTable' },
ES_USE_KEYWORD_FILTERS: { 'Fn::If': ['EmbeddingsEnable', 'false', 'true'] },
EMBEDDINGS_ENABLE: { 'Fn::If': ['EmbeddingsEnable', 'true', 'false'] },
EMBEDDINGS_MAX_TOKEN_LIMIT: { 'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'MaxTokens'] }, ''] },
EMBEDDINGS_SCORE_THRESHOLD: { 'Fn::If': ['EmbeddingsBedrock', 0.7, 0.85] },
EMBEDDINGS_TEXT_PASSAGE_SCORE_THRESHOLD: { 'Fn::If': ['EmbeddingsBedrock', 0.65, 0.8] },
NATIVE_LANGUAGE: { Ref: 'Language' },
ALT_SEARCH_KENDRA_INDEXES: {Ref: 'AltSearchKendraIndexes'},
ALT_SEARCH_KENDRA_INDEX_AUTH: {Ref: 'AltSearchKendraIndexAuth'},
KENDRA_FAQ_INDEX: {Ref: 'KendraFaqIndexId'},
KENDRA_WEB_PAGE_INDEX: {Ref: 'KendraWebPageIndexId'},
LLM_API: { Ref: 'LLMApi' },
LLM_GENERATE_QUERY_ENABLE: { 'Fn::If': ['LLMEnable', 'true', 'false'] },
LLM_QA_ENABLE: { 'Fn::If': ['LLMEnable', 'true', 'false'] },
LLM_GENERATE_QUERY_PROMPT_TEMPLATE: defaultGenerateQueryPromptTemplate,
LLM_GENERATE_QUERY_SYSTEM_PROMPT: '',
LLM_QA_PROMPT_TEMPLATE: defaultQAPromptTemplate,
LLM_QA_SYSTEM_PROMPT: defaultQuerySystemPrompt,
LLM_GENERATE_QUERY_MODEL_PARAMS: { 'Fn::If': ['LLMBedrock', defaultModelParams, '{}'] },
LLM_QA_MODEL_PARAMS: { 'Fn::If': ['LLMBedrock', defaultModelParams, '{}'] },
LLM_PROMPT_MAX_TOKEN_LIMIT: { 'Fn::If': ['LLMBedrock', 100000, ''] },
LLM_QA_NO_HITS_REGEX: defaultLlmNoHitsRegex,
KNOWLEDGE_BASE_PROMPT_TEMPLATE: defaultKnowledgeBaseTemplate,
EMBEDDINGS_MODEL_ID: { 'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'ModelID'] }, ''] },
LLM_MODEL_ID: { 'Fn::If': ['LLMBedrock', { 'Ref' : 'LLMBedrockModelId' }, ''] },
KNOWLEDGE_BASE_MODEL_ID: { 'Fn::If': ['BedrockKnowledgeBaseEnable', {'Ref' : 'BedrockKnowledgeBaseModel'}, ''] },
KNOWLEDGE_BASE_ID: { 'Fn::If': ['BedrockKnowledgeBaseEnable', {'Ref' : 'BedrockKnowledgeBaseId'}, ''] },
LLM_STREAMING_ENABLED: { 'Fn::If': ['StreamingEnabled', 'true', 'false'] },
STREAMING_TABLE: { 'Fn::If': ['StreamingEnabled', { 'Fn::GetAtt': ['StreamingStack', 'Outputs.StreamingDynamoDbTable'] }, ''] },
DefaultSettingsParameter: { Ref: 'DefaultQnABotSettings' },
PrivateSettingsParameter: { Ref: 'PrivateQnABotSettings' },
CustomSettingsParameter: { Ref: 'CustomQnABotSettings' },
},
},
};
================================================
FILE: source/templates/master/examples.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
ExamplesStack: {
Type: 'AWS::CloudFormation::Stack',
Condition: 'BuildExamples',
Properties: {
TemplateURL: { 'Fn::Sub': 'https://${BootstrapBucket}.s3.${AWS::Region}.amazonaws.com/${BootstrapPrefix}/templates/examples.json' },
Parameters: {
QnAType: { 'Fn::GetAtt': ['Var', 'QnAType'] },
QuizType: { 'Fn::GetAtt': ['Var', 'QuizType'] },
Index: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ResponseBotStackName: { 'Fn::GetAtt': ['Var', 'ResponseBotStackName'] },
ESAddress: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
BootstrapBucket: { Ref: 'BootstrapBucket' },
BootstrapPrefix: { Ref: 'BootstrapPrefix' },
FeedbackKinesisFirehose: { 'Fn::GetAtt': ['FeedbackKinesisFirehose', 'Arn'] },
FeedbackKinesisFirehoseName: { Ref: 'FeedbackKinesisFirehose' },
CFNLambda: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
CFNLambdaRole: { 'Fn::GetAtt': ['CFNLambdaRole', 'Arn'] },
S3Clean: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
ApiUrlName: { 'Fn::GetAtt': ['ApiUrl', 'Name'] },
AssetBucket: { Ref: 'AssetBucket' },
FulfillmentLambdaRole: { Ref: 'FulfillmentLambdaRole' },
QIDLambdaArn: { 'Fn::GetAtt': ['ESQidLambda', 'Arn'] },
VPCSubnetIdList: { 'Fn::Join': [',', { Ref: 'VPCSubnetIdList' }] },
VPCSecurityGroupIdList: { 'Fn::Join': [',', { Ref: 'VPCSecurityGroupIdList' }] },
XraySetting: { Ref: 'XraySetting' },
InstallLexResponseBots: { Ref: 'InstallLexResponseBots' },
AwsSdkLayerLambdaLayer: { Ref: 'AwsSdkLayerLambdaLayer' },
LogRetentionPeriod: { Ref: 'LogRetentionPeriod' },
},
},
},
};
================================================
FILE: source/templates/master/exportstack.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
ExportStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: { 'Fn::Sub': 'https://${BootstrapBucket}.s3.${AWS::Region}.amazonaws.com/${BootstrapPrefix}/templates/export.json' },
Parameters: {
SettingsTable: { Ref: 'SettingsTable' },
ContentDesignerOutputBucket: { Ref: 'ContentDesignerOutputBucket' },
CFNLambda: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
CFNInvokePolicy: { Ref: 'CFNInvokePolicy' },
S3Clean: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
BootstrapBucket: { Ref: 'BootstrapBucket' },
BootstrapPrefix: { Ref: 'BootstrapPrefix' },
VarIndex: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
EsEndpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
EsProxyLambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
ExportBucket: { Ref: 'ExportBucket' },
VPCSubnetIdList: { 'Fn::Join': [',', { Ref: 'VPCSubnetIdList' }] },
VPCSecurityGroupIdList: { 'Fn::Join': [',', { Ref: 'VPCSecurityGroupIdList' }] },
XraySetting: { Ref: 'XraySetting' },
Api: { Ref: 'API' },
ApiRootResourceId: { 'Fn::GetAtt': ['API', 'RootResourceId'] },
Stage: { Ref: 'Stage' },
ApiDeploymentId: { Ref: 'Deployment' },
AwsSdkLayerLambdaLayer: { Ref: 'AwsSdkLayerLambdaLayer' },
QnABotCommonLambdaLayer: { Ref: 'QnABotCommonLambdaLayer' },
LexVersion: 'V2',
// Lex V2
LexV2BotName: { 'Fn::GetAtt': ['LexV2Bot', 'botName'] },
LexV2BotId: { 'Fn::GetAtt': ['LexV2Bot', 'botId'] },
LexV2BotAlias: { 'Fn::GetAtt': ['LexV2Bot', 'botAlias'] },
LexV2BotAliasId: { 'Fn::GetAtt': ['LexV2Bot', 'botAliasId'] },
LexV2BotLocaleIds: { 'Fn::GetAtt': ['LexV2Bot', 'botLocaleIds'] },
KendraFaqIndexId: { Ref: 'KendraFaqIndexId' },
KendraWebPageIndexId: { Ref: 'KendraWebPageIndexId' },
LogRetentionPeriod: { Ref: 'LogRetentionPeriod' },
},
},
},
};
================================================
FILE: source/templates/master/importstack.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
ImportStack: {
Type: 'AWS::CloudFormation::Stack',
DependsOn: ['PreUpgradeExport'],
Properties: {
TemplateURL: { 'Fn::Sub': 'https://${BootstrapBucket}.s3.${AWS::Region}.amazonaws.com/${BootstrapPrefix}/templates/import.json' },
Parameters: {
ContentDesignerOutputBucket: { Ref: 'ContentDesignerOutputBucket' },
CFNLambda: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
CFNInvokePolicy: { Ref: 'CFNInvokePolicy' },
S3Clean: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
BootstrapBucket: { Ref: 'BootstrapBucket' },
BootstrapPrefix: { Ref: 'BootstrapPrefix' },
EsEndpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
EsArn: { 'Fn::GetAtt': ['ESVar', 'ESArn'] },
EsProxyLambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
ImportBucket: { Ref: 'ImportBucket' },
ExportBucket: { Ref: 'ExportBucket' },
VarIndex: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
MetricsIndex: { 'Fn::GetAtt': ['Var', 'MetricsIndex'] },
FeedbackIndex: { 'Fn::GetAtt': ['Var', 'FeedbackIndex'] },
VPCSubnetIdList: { 'Fn::Join': [',', { Ref: 'VPCSubnetIdList' }] },
VPCSecurityGroupIdList: { 'Fn::Join': [',', { Ref: 'VPCSecurityGroupIdList' }] },
XraySetting: { Ref: 'XraySetting' },
AwsSdkLayerLambdaLayer: { Ref: 'AwsSdkLayerLambdaLayer' },
CommonModulesLambdaLayer: { Ref: 'CommonModulesLambdaLayer' },
EsProxyLambdaLayer: { Ref: 'EsProxyLambdaLayer' },
QnABotCommonLambdaLayer: { Ref: 'QnABotCommonLambdaLayer' },
EmbeddingsLambdaArn: { Ref: 'EmbeddingsLambdaArn' },
EmbeddingsApi: { Ref: 'EmbeddingsApi' },
EmbeddingsLambdaDimensions: { Ref: 'EmbeddingsLambdaDimensions' },
EmbeddingsBedrockModelId: { Ref : 'EmbeddingsBedrockModelId' },
LogRetentionPeriod: { Ref: 'LogRetentionPeriod' },
SettingsTable: { Ref: 'SettingsTable'},
},
},
},
};
================================================
FILE: source/templates/master/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const files = [
require('./UpgradeAutoExport'),
require('./assets'),
require('./bucket'),
require('./cfn'),
require('./cognito'),
require('./config'),
require('./dashboard'),
require('./dynamodb'),
require('./examples'),
require('./exportstack'),
require('./importstack'),
require('./lambda-layers'),
require('./lambda'),
require('./lex'),
require('./lex-build'),
require('./lexv2-build'),
require('./opensearch'),
require('./policies.json'),
require('./proxy-es'),
require('./proxy-lex'),
require('./roles.json'),
require('./routes'),
require('./s3'),
require('./s3-clean'),
require('./schemaLambda'),
require('./settings'),
require('./signup'),
require('./solution-helper'),
require('./streamingstack'),
require('./tstallstack'),
require('./var'),
];
const mappings = fs
.readdirSync(`${__dirname}/mappings`)
.map((x) => require(`./mappings/${x}`));
module.exports = {
Resources: _.assign.apply({}, files),
AWSTemplateFormatVersion: '2010-09-09',
Description: `(SO0189-ext) QnABot with admin and client websites - Version v${process.env.npm_package_version}`,
Mappings: _.assign.apply({}, mappings),
Outputs: {
CognitoEndpoint: {
Value: { 'Fn::GetAtt': ['DesignerLogin', 'Domain'] },
},
UserRole: {
Value: { Ref: 'UserRole' },
},
ImportBucket: {
Value: { Ref: 'ImportBucket' },
},
LexV2BotName: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botName'] },
},
LexV2BotId: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botId'] },
},
LexV2BotAlias: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botAlias'] },
},
LexV2BotAliasId: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botAliasId'] },
},
LexV2Intent: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botIntent'] },
},
LexV2IntentFallback: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botIntentFallback'] },
},
LexV2BotLocaleIds: {
Value: { 'Fn::GetAtt': ['LexV2Bot', 'botLocaleIds'] },
},
CloudWatchDashboardURL: {
Value: {
'Fn::Join': [
'',
[
'https://console.aws.amazon.com/cloudwatch/home?',
'region=',
{ Ref: 'AWS::Region' },
'#dashboards:name=',
{ Ref: 'dashboard' },
],
],
},
},
UserPoolURL: {
Value: {
'Fn::Join': [
'',
[
'https://console.aws.amazon.com/cognito/users/',
'?region=',
{ Ref: 'AWS::Region' },
'#/pool/',
{ Ref: 'UserPool' },
'/details',
],
],
},
},
Bucket: {
Value: { Ref: 'Bucket' },
},
IdPool: {
Value: { Ref: 'IdPool' },
},
ApiEndpoint: {
Value: { 'Fn::GetAtt': ['ApiUrl', 'Name'] },
},
ESProxyLambda: {
Value: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
},
CFNESProxyLambda: {
Value: { 'Fn::GetAtt': ['ESCFNProxyLambda', 'Arn'] },
},
ContentDesignerURL: {
Value: {
'Fn::Join': ['', [{ 'Fn::GetAtt': ['ApiUrl', 'Name'] }, '/pages/designer']],
},
},
ClientURL: {
Value: {
'Fn::If': [
'Public',
{ 'Fn::GetAtt': ['Urls', 'Client'] },
{
'Fn::Join': ['', [{ 'Fn::GetAtt': ['ApiUrl', 'Name'] }, '/pages/client']],
},
],
},
},
ApiId: {
Value: { Ref: 'API' },
},
UserPool: {
Value: { Ref: 'UserPool' },
},
DesignerClientId: {
Value: { Ref: 'ClientDesigner' },
},
ClientClientId: {
Value: { Ref: 'ClientClient' },
},
OpenSearchDomainEndpoint: {
Value: {
'Fn::Join': [
'',
['https://', { 'Fn::GetAtt': ['ESVar', 'ESAddress'] }]
]
},
},
OpenSearchQnAType: {
Value: { 'Fn::GetAtt': ['Var', 'QnAType'] },
},
OpenSearchQuizType: {
Value: { 'Fn::GetAtt': ['Var', 'QuizType'] },
},
OpenSearchIndex: {
Value: { 'Fn::GetAtt': ['Var', 'index'] },
},
UsersTable: {
Value: { Ref: 'UsersTable' },
},
DefaultUserPoolJwksUrlParameterName: {
Value: { Ref: 'DefaultUserPoolJwksUrl' },
},
FeedbackSNSTopic: {
Condition: 'BuildExamples',
Value: { 'Fn::GetAtt': ['ExamplesStack', 'Outputs.FeedbackSNSTopic'] },
},
MetricsBucket: {
Value: { Ref: 'MetricsBucket' },
},
TestAllBucket: {
Value: { Ref: 'TestAllBucket' },
},
ContentDesignerOutputBucket: {
Value: { Ref: 'ContentDesignerOutputBucket' },
},
StreamingWebSocketEndpoint: {
Condition: 'StreamingEnabled',
Value: { 'Fn::GetAtt': ['StreamingStack', 'Outputs.StreamingWebSocketEndpoint'] }
},
SettingsTable: {
Value: { Ref: 'SettingsTable' },
},
},
Parameters: {
OpenSearchName: {
Type: 'String',
Description: 'Set this to the target Amazon OpenSearch domain name to use an existing OpenSearch service. Set to \'EMPTY\' to provision a new Amazon OpenSearch service',
Default: 'EMPTY',
AllowedPattern: '([^ ]+)|(EMPTY)',
ConstraintDescription: 'Must be a valid Amazon OpenSearch domain name or \'EMPTY\'',
},
OpenSearchNodeInstanceType: {
Type: 'String',
Description:
'OpenSearch instance type for data nodes in the domain. Default recommendation for production deployments is m6g.large.search (see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html for other options).',
Default: 'm6g.large.search',
AllowedPattern: '^\\w+\\.\\w+\\.search$',
ConstraintDescription: 'Must be a valid OpenSearch instance type',
},
OpenSearchFineGrainAccessControl: {
Type: 'String',
AllowedValues: ['FALSE', 'TRUE'],
Description:
'Set to FALSE if Fine-grained access control does not need to be enabled by default. Once fine-grained access control is enabled, it cannot be disabled. Please note that it may take an additional 30-60 minutes for AWS OpenSearch Service to apply these settings to the OpenSearch domain after the stack has been deployed. (see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html for additional details).',
ConstraintDescription: 'Allowed Values are FALSE or TRUE',
Default: 'TRUE',
},
AdminUserSignUp: {
Type: 'String',
Description: 'Set to TRUE if only the administrator is allowed to create user profiles in Amazon Cognito',
AllowedValues: ['FALSE', 'TRUE'],
ConstraintDescription: 'Allowed Values are FALSE or TRUE',
Default: 'TRUE',
},
ApprovedDomain: {
Type: 'String',
Description:
'If QnABot is private, restrict user sign up to users whos email domain matches this domain. eg. amazon.com',
Default: '',
AllowedPattern: '(.+\\..+)*|(NONE)|(EMPTY)',
ConstraintDescription: 'Must be a valid domain name eg. example.com',
},
Email: {
Type: 'String',
Description:
'Email address for the admin user. This email address will receive a temporary password to access the QnABot on AWS content designer.',
AllowedPattern: '.+\\@.+\\..+',
ConstraintDescription: 'Must be valid email address eg. johndoe@example.com',
},
Username: {
Type: 'String',
Description: 'This username will be used to sign in to QnABot on AWS content designer console.',
Default: 'Admin',
AllowedPattern: '[^ ]+',
ConstraintDescription: 'Must not be empty or contain spaces',
},
KendraWebPageIndexId: {
Type: 'String',
Description:
'Optional: Id of the Amazon Kendra index to use for the web crawler, a custom data source will automatically be added to the specified index. Also use this index id in AltSearchKendraIndexes to enable fallback.',
Default: '',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a valid Amazon Kendra index id or left blank',
},
KendraFaqIndexId: {
Type: 'String',
Description:
'Optional: Id of the Amazon Kendra Index to use for syncing OpenSearch questions and answers',
Default: '',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a valid Amazon Kendra index id or left blank',
},
AltSearchKendraIndexes: {
Type: 'String',
Description:
'Optional: A comma separated String value specifying ids of one or more Amazon Kendra indexes to be used for Kendra fallback',
Default: '',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a list of valid Amazon Kendra index id(s) or left blank',
},
AltSearchKendraIndexAuth: {
Type: 'String',
Description: 'Set to true if using Kendra Index(es) with access control enabled. This tells QnABot to pass an authentication token to Kendra Index(es) used for Kendra fallback if it is available.',
AllowedValues: ['true', 'false'],
Default: 'false',
},
BootstrapBucket: {
Type: 'String',
Description: 'Name of the S3 bucket used in bootstrapping resources',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a valid S3 bucket name or left blank',
},
BootstrapPrefix: {
Type: 'String',
Description: 'S3 key prefix to the bootstrapping resources',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a valid S3 key prefix or left blank',
},
BuildExamples: {
Type: 'String',
Description: 'Experimental (Development ONLY): Set to TRUE to deploy the QnABot Examples Stack. Note: Selecting FALSE will not the deploy the QnABot Examples Stack. This will limit also disable the feedback functionality and there will be no predefined examples questions set.',
Default: 'TRUE',
AllowedValues: ['TRUE', 'FALSE'],
},
PublicOrPrivate: {
Type: 'String',
Description:
'Choose whether access to the QnABot client should be publicly available or restricted to users in QnABot UserPool.',
AllowedValues: ['PUBLIC', 'PRIVATE'],
Default: 'PRIVATE',
},
Language: {
Type: 'String',
Description: 'Choose the primary Language for your QnABot deployment. Note: Picking non-English may correspond with limited functionalities',
AllowedValues: ['Arabic', 'Armenian', 'Basque', 'Bengali', 'Brazilian', 'Bulgarian', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Estonian', 'Finnish', 'French', 'Galician', 'German', 'Greek', 'Hindi', 'Hungarian', 'Indonesian', 'Irish', 'Italian', 'Latvian', 'Lithuanian', 'Norwegian', 'Portuguese', 'Romanian', 'Russian', 'Sorani', 'Spanish', 'Swedish', 'Turkish', 'Thai'],
Default: 'English',
},
OpenSearchNodeCount: {
Type: 'String',
Description:
'Number of data nodes in Amazon OpenSearch Service domain - \'4\' is recommended for fault tolerant production deployments.',
AllowedValues: ['1', '2', '4'],
Default: '4',
},
OpenSearchEBSVolumeSize: {
Type: 'Number',
Description:
'Size in GB of each EBS volume attached to OpenSearch node instances - \'10\' is the minimum default volume size.',
Default: 10,
MinValue: 10,
},
FulfillmentConcurrency: {
Type: 'Number',
Description: 'The amount of provisioned concurrency for the fulfillment Lambda function - see: https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html',
Default: 0,
MinValue: 0,
},
VPCSubnetIdList: {
Type: 'CommaDelimitedList',
Description: 'Set to a list of Subnet IDs belonging to the target VPC you want to deploy QnABot on AWS in.',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a list of valid subnet IDs',
Default: '',
},
VPCSecurityGroupIdList: {
Type: 'CommaDelimitedList',
Description: 'Set to a list of Security Group IDs used by QnABot when deployed within a VPC.',
AllowedPattern: '[^ ]*',
ConstraintDescription: 'Must be a list of valid security group IDs',
Default: '',
},
LexV2BotLocaleIds: {
Description:
'Languages for QnABot on AWS voice interaction using LexV2. Specify as a comma separated list of valid Locale IDs without empty spaces - see https://github.com/aws-solutions/qnabot-on-aws/blob/main/source/docs/multilanguage_support/README.md#supported-languages',
Type: 'String',
Default: 'en_US,es_US,fr_CA',
AllowedPattern: '[^ ]+',
ConstraintDescription: 'Must be a valid comma separated list of Locale IDs',
},
InstallLexResponseBots: {
Description:
'You can configure your chatbot to ask questions and process your end user\'s answers for surveys, quizzes,... (Elicit Response Feature). If the Elicit Response feature is not needed, choose \'false\' to skip the sample Lex Response Bot installation - see https://docs.aws.amazon.com/solutions/latest/qnabot-on-aws/configuring-the-chatbot-to-ask-the-questions-and-use-response-bots.html',
Type: 'String',
AllowedValues: ['true', 'false'],
Default: 'true',
},
XraySetting: {
Type: 'String',
Description: 'Configure Lambdas with X-Ray enabled',
AllowedValues: ['FALSE', 'TRUE'],
Default: 'FALSE',
ConstraintDescription: 'Allowed Values are FALSE or TRUE',
},
OpenSearchDashboardsRetentionMinutes: {
Type: 'Number',
Description:
'To conserve storage in Amazon OpenSearch, metrics and feedback data used to populate the OpenSearch dashboards are automatically deleted after this period (default 43200 minutes = 30 days). Monitor \'Free storage space\' for your OpenSearch domain to ensure that you have sufficient space available to store data for the desired retention period.',
Default: 43200,
MinValue: 0,
},
EmbeddingsApi: {
Type: 'String',
Description:
'Enable QnABot semantics search using Embeddings from a pre-trained Large Language Model. To use a custom LAMBDA function, provide additional parameters below.',
AllowedValues: ['DISABLED', 'BEDROCK', 'LAMBDA'],
Default: 'DISABLED',
},
EmbeddingsBedrockModelId: {
Type: 'String',
Description:
'Required when EmbeddingsApi is BEDROCK.',
AllowedValues: [
'amazon.titan-embed-text-v1',
'amazon.titan-embed-text-v2',
'amazon.nova-2-multimodal-embeddings-v1',
'cohere.embed-english-v3',
'cohere.embed-multilingual-v3',
'global.cohere.embed-v4'
],
Default: 'amazon.nova-2-multimodal-embeddings-v1',
},
EmbeddingsLambdaArn: {
Type: 'String',
AllowedPattern: '^(|arn:aws:lambda:.*)$',
Description:
'Required when EmbeddingsApi is LAMBDA. Provide the ARN for a Lambda function that takes JSON {"inputtext":"string"}, and returns JSON {"embedding":[...]}',
Default: '',
ConstraintDescription: 'Must be a valid Lambda ARN or leave blank',
},
EmbeddingsLambdaDimensions: {
Type: 'Number',
MinValue: 1,
Description:
'Required when EmbeddingsApi is LAMBDA. Provide number of dimensions for embeddings returned by the EmbeddingsLambda function specified above.',
Default: 1536,
},
LLMApi: {
Type: 'String',
Description:
'Optionally enable QnABot on AWS question disambiguation and generative question answering using an LLM. Selecting the LAMBDA option allows for configuration with other LLMs.',
AllowedValues: ['DISABLED', 'LAMBDA', 'BEDROCK'],
Default: 'DISABLED',
},
LLMBedrockModelId: {
Type: 'String',
Description:
'Required when LLMApi is BEDROCK. Provide a valid foundation model ID or inference profile ID. See (https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) and (https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html).',
AllowedPattern:
'^([\\w\\.-]+:[0-9]+|[\\w\\.-]+)$',
ConstraintDescription:
'Must be a valid Bedrock foundation model ID or inference profile ID.',
Default: 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
},
EnableStreaming: {
Type: 'String',
Description: 'Set to TRUE to deploy the streaming resources using for LLMs.',
Default: 'FALSE',
AllowedValues: ['TRUE', 'FALSE'],
},
BedrockKnowledgeBaseId: {
Type: 'String',
Description:
'Optional: ID of an existing Bedrock knowledge base. This setting enables the use of Bedrock knowledge bases as a fallback mechanism when a match is not found in OpenSearch.',
AllowedPattern: '[0-9A-Z]{10}|^$',
Default: '',
ConstraintDescription: 'Must be a valid Bedrock knowledge base id or leave blank',
},
BedrockKnowledgeBaseModel: {
Type: 'String',
Description:
'Required if BedrockKnowledgeBaseId is not empty. Provide a valid foundation model ID or inference profile id to use with the Bedrock knowledge base. See (https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) and (https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html).',
AllowedPattern:
'^([\\w\\.-]+:[0-9]+|[\\w\\.-]+)$',
ConstraintDescription:
'Must be a valid Bedrock foundation model ID or inference profile ID.',
Default: 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
},
LLMLambdaArn: {
Type: 'String',
AllowedPattern: '^(|arn:aws:lambda:.*)$',
Description:
'Required if LLMApi is LAMBDA. Provide ARN for a Lambda function that takes JSON {"prompt":"string", "settings":{key:value,..}}, and returns JSON {"generated_text":"string"}',
Default: '',
ConstraintDescription: 'Must be a valid Lambda ARN or leave blank',
},
LogRetentionPeriod: {
Type: 'Number',
Description: 'Optional: The number of days to keep logs before expiring. If you would like your logs to never expire, leave this value as 0.',
Default: 0,
AllowedValues: [
0, 1, 3, 5, 7, 14 , 30 , 60 , 90 , 120 , 150 , 180 , 365 , 400 , 545 , 731 , 1096 , 1827 , 2192 , 2557 , 2922 , 3288 , 3653
],
MinValue: 0,
},
OpenSearchDedicatedMasterNodes: {
Type: 'String',
Description: 'Enable OpenSearch add dedicated master nodes to increase cluster stability. Please note that deploying additional nodes will increase cost, see - https://aws.amazon.com/opensearch-service/pricing/',
Default: 'DISABLED',
AllowedValues: ['DISABLED', 'ENABLED'],
},
OpenSearchMasterNodeInstanceType: {
Type: 'String',
Description:
'Required when OpenSearchDedicatedMasterNodes is ENABLED. OpenSearch instance type for master nodes in the domain. Default recommendation for production deployments is m6g.large.search (see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html for other options).',
Default: 'm6g.large.search',
AllowedPattern: '^\\w+\\.\\w+\\.search$',
ConstraintDescription: 'Must be a valid OpenSearch instance type',
},
OpenSearchMasterNodeCount: {
Type: 'String',
Description:
'Required when OpenSearchDedicatedMasterNodes is ENABLED. Number of dedicated master nodes to add in your Amazon OpenSearch Service domain. \'3\' is the minimum default value. See - https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-dedicatedmasternodes.html#dedicatedmasternodes-number',
AllowedValues: ['3', '5'],
Default: '3',
},
},
Conditions: {
Public: { 'Fn::Equals': [{ Ref: 'PublicOrPrivate' }, 'PUBLIC'] },
AdminSignUp: { 'Fn::Equals': [{ Ref: 'AdminUserSignUp' }, 'TRUE'] },
XRAYEnabled: { 'Fn::Equals': [{ Ref: 'XraySetting' }, 'TRUE'] },
StreamingEnabled: { 'Fn::Equals': [{ Ref: 'EnableStreaming' }, 'TRUE'] },
FGACEnabled: { 'Fn::Equals': [{ Ref: 'OpenSearchFineGrainAccessControl' }, 'TRUE'] },
Domain: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'ApprovedDomain' }, 'NONE'] }] },
BuildExamples: { 'Fn::Equals': [{ Ref: 'BuildExamples' }, 'TRUE'] },
CreateDomain: { 'Fn::Equals': [{ Ref: 'OpenSearchName' }, 'EMPTY'] },
DontCreateDomain: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'OpenSearchName' }, 'EMPTY'] }] },
VPCEnabled: {
'Fn::Not': [
{
'Fn::Equals': ['', { 'Fn::Join': ['', { Ref: 'VPCSecurityGroupIdList' }] }],
},
],
},
CreateConcurrency: {
'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'FulfillmentConcurrency' }, '0'] }],
},
SingleNode: { 'Fn::Equals': [{ Ref: 'OpenSearchNodeCount' }, '1'] },
MasterNodesEnabled: { 'Fn::Equals': [{ Ref: 'OpenSearchDedicatedMasterNodes' }, 'ENABLED'] },
BedrockKnowledgeBaseEnable: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'BedrockKnowledgeBaseId' }, ''] }] },
BedrockEnable: { 'Fn::Or': [{ 'Fn::Equals': [{ Ref: 'LLMApi' }, 'BEDROCK'] }, { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] }, { Condition: 'BedrockKnowledgeBaseEnable' }] },
EmbeddingsEnable: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'DISABLED'] }] },
EmbeddingsBedrock: { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] },
EmbeddingsLambda: { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'LAMBDA'] },
EmbeddingsLambdaArn: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsLambdaArn' }, ''] }] },
LLMEnable: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'LLMApi' }, 'DISABLED'] }] },
LLMBedrock: { 'Fn::Equals': [{ Ref: 'LLMApi' }, 'BEDROCK'] },
LLMLambda: { 'Fn::Equals': [{ Ref: 'LLMApi' }, 'LAMBDA'] },
LLMLambdaArn: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'LLMLambdaArn' }, ''] }] },
SolutionHelperSendAnonymizedDataToAWS: { 'Fn::Equals': [{ 'Fn::FindInMap': ['SolutionHelperAnonymizedData', 'SendAnonymizedData', 'Data'] }, 'Yes'] },
KendraPluginsEnabled: {
'Fn::Or': [
{ 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'KendraWebPageIndexId' }, ''] }] },
{ 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'KendraFaqIndexId' }, ''] }] },
{ 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'AltSearchKendraIndexes' }, ''] }] },
],
},
LogRetentionPeriodIsNotZero: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'LogRetentionPeriod' }, 0] }] },
},
Rules: {
RequireLambdaArnForLambdaEmbeddingsApi: {
RuleCondition: {
'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'LAMBDA'],
},
Assertions: [
{
Assert: {
'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsLambdaArn' }, ''] }],
},
AssertDescription: 'EmbeddingsLambdaArn is required when EmbeddingsApi is set to LAMBDA.',
},
],
},
},
Metadata: {
'AWS::CloudFormation::Interface': {
ParameterGroups: [
{
Label: {
default: 'Step 2A: Set Basic Chatbot Parameters (required)',
},
Parameters: [
'Email',
'Username',
'PublicOrPrivate',
'Language',
'OpenSearchName',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'LexV2BotLocaleIds',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting',
],
},
{
Label: {
default: 'Step 2B: Set VPC parameters to deploy QnABot in an existing VPC (optional)',
},
Parameters: [
'VPCSubnetIdList',
'VPCSecurityGroupIdList',
],
},
{
Label: {
default: 'Step 2C: Enable LLM for Semantic Search with Embeddings (optional)',
},
Parameters: [
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
],
},
{
Label: {
default: 'Step 2D: Enable LLM Retrieval and generative text question answering to use with Fallback Option (optional)',
},
Parameters: [
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'EnableStreaming'
],
},
{
Label: {
default: 'Step 2E: Select Data Sources as Fallback Option (optional)',
},
Parameters: [
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'AltSearchKendraIndexAuth',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
],
},
{
Label: {
default: 'Step 2F: Set miscellaneous settings (optional)',
},
Parameters: [
'AdminUserSignUp',
'ApprovedDomain',
'BootstrapBucket',
'BootstrapPrefix',
'BuildExamples',
'LogRetentionPeriod',
],
},
],
},
},
}
================================================
FILE: source/templates/master/index.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const indexModule = require('./index');
function create() {
const file = `${__dirname}/`;
return require(file);
}
describe('Verify master template is correct', () => {
it('renders master template correctly', () => {
const template = create();
expect(template).toMatchSnapshot({
Resources: {
AssetZipVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
AwsSdkLayerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
CFNVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
CfnLambdaLayerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
CommonModulesLayerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
Deployment: {
Properties: {
buildDate: expect.any(Date),
},
},
ESProxyCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
EsProxyLayerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
ESWarmerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
FulfillmentCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
InfoVar: {
Properties: {
BuildDate: expect.any(Date),
BuildDateString: expect.any(String),
Version: expect.any(String),
},
},
LexBuildCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
LexV2Bot: {
Properties: {
BuildDate: expect.any(String),
},
},
Lexv2BotCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
QnABotCommonLayerCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
SchemaLambdaCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
Unzip: {
Properties: {
buildDate: expect.any(Date),
},
},
SolutionHelperCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
S3ClearCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
},
});
});
test('that all template parameters have descriptions', () => {
const parameters = indexModule.Parameters;
const keys = Object.keys(parameters);
keys.forEach((key) => {
const description = parameters[key].Description;
if (!description) {
throw new Error(`No description defined for parameter: ${key}`);
}
});
});
test('that all string template parameters have allowed values or patterns', () => {
const parameters = indexModule.Parameters;
const keys = Object.keys(parameters);
keys.forEach((key) => {
if (parameters[key].Type == 'String') {
const allowedValuesOrPatterns = parameters[key].AllowedValues
|| parameters[key].AllowedPattern;
if (!allowedValuesOrPatterns) {
throw new Error(`No allowed values or patterns defined for parameter: ${key}`);
}
}
});
});
test('that all number template parameters have minimum values defined', () => {
const parameters = indexModule.Parameters;
const keys = Object.keys(parameters);
keys.forEach((key) => {
if (parameters[key].Type == 'Number') {
if (parameters[key].MinValue == null) {
throw new Error(`No minimum value defined for parameter: ${key}`);
}
}
});
});
test('that all String parameters have constraint descriptions', () => {
const parameters = indexModule.Parameters;
const keys = Object.keys(parameters);
keys.forEach((key) => {
if (parameters[key].Type == 'String' && !parameters[key].AllowedValues) {
const constraintDescription = parameters[key].ConstraintDescription;
if (!constraintDescription) {
throw new Error(`No constraint description defined for parameter: ${key}`);
}
}
});
});
});
================================================
FILE: source/templates/master/lambda-layers.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
CommonModulesLayerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/common-modules-layer.zip' },
BuildDate: new Date().toISOString(),
},
},
CommonModulesLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
LayerName: {
'Fn::Join': [
'-',
[
'CommonModules',
{ 'Fn::Select': ['0', { 'Fn::Split': ['-', { Ref: 'AWS::StackName' }] }] }
],
],
},
Content: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: {
'Fn::Sub': '${BootstrapPrefix}/lambda/common-modules-layer.zip',
},
S3ObjectVersion: { Ref: 'CommonModulesLayerCodeVersion' },
},
CompatibleRuntimes: [process.env.npm_package_config_lambdaRuntime],
},
},
QnABotCommonLayerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/qnabot-common-layer.zip' },
BuildDate: new Date().toISOString(),
},
},
QnABotCommonLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
LayerName: {
'Fn::Join': [
'-',
[
'QnABotCommon',
{ 'Fn::Select': ['0', { 'Fn::Split': ['-', { Ref: 'AWS::StackName' }] }] },
],
],
},
Content: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: {
'Fn::Sub': '${BootstrapPrefix}/lambda/qnabot-common-layer.zip',
},
S3ObjectVersion: { Ref: 'QnABotCommonLayerCodeVersion' },
},
CompatibleRuntimes: [process.env.npm_package_config_lambdaRuntime],
},
},
AwsSdkLayerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/aws-sdk-layer.zip' },
BuildDate: new Date().toISOString(),
},
},
AwsSdkLayerLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
Content: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/aws-sdk-layer.zip' },
S3ObjectVersion: { Ref: 'AwsSdkLayerCodeVersion' },
},
LayerName: {
'Fn::Join': [
'-',
[
'AwsSdk',
{ 'Fn::Select': ['0', { 'Fn::Split': ['-', { Ref: 'AWS::StackName' }] }] },
],
],
},
CompatibleRuntimes: [process.env.npm_package_config_lambdaRuntime],
},
},
CfnLambdaLayerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/cfn-lambda-layer.zip' },
BuildDate: new Date().toISOString(),
},
},
CfnLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
LayerName: {
'Fn::Join': [
'-',
[
'CfnLambdaModule',
{ 'Fn::Select': ['0', { 'Fn::Split': ['-', { Ref: 'AWS::StackName' }] }] },
],
],
},
Content: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/cfn-lambda-layer.zip' },
S3ObjectVersion: { Ref: 'CfnLambdaLayerCodeVersion' },
},
CompatibleRuntimes: [process.env.npm_package_config_lambdaRuntime],
},
},
EsProxyLayerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/es-proxy-layer.zip' },
BuildDate: new Date().toISOString(),
},
},
EsProxyLambdaLayer: {
Type: 'AWS::Lambda::LayerVersion',
Properties: {
LayerName: {
'Fn::Join': [
'-',
[
'EsProxy',
{ 'Fn::Select': ['0', { 'Fn::Split': ['-', { Ref: 'AWS::StackName' }] }] }
],
],
},
Content: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/es-proxy-layer.zip' },
S3ObjectVersion: { Ref: 'EsProxyLayerCodeVersion' },
},
CompatibleRuntimes: [process.env.npm_package_config_lambdaRuntime],
},
},
};
================================================
FILE: source/templates/master/lambda.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const files = [
require('./UpgradeAutoExport'),
require('./assets'),
require('./bucket'),
require('./cfn'),
require('./cognito'),
require('./dashboard'),
require('./dynamodb'),
require('./examples'),
require('./exportstack'),
require('./importstack'),
require('./lambda-layers'),
require('./lex'),
require('./lex-build'),
require('./lexv2-build'),
require('./opensearch'),
require('./policies.json'),
require('./proxy-es'),
require('./proxy-lex'),
require('./roles.json'),
require('./routes'),
require('./s3'),
require('./s3-clean'),
require('./schemaLambda'),
require('./settings'),
require('./signup'),
require('./solution-helper'),
require('./tstallstack'),
require('./var'),
];
const lambdas = [];
_.forEach(_.assign.apply({}, files), (value, key) => {
if (value.Type === 'AWS::Lambda::Function') {
const type = _.fromPairs(value.Properties.Tags.map((x) => [x.Key, x.Value])).Type;
if (type === 'Api' || type == 'Service') {
lambdas.push([`InvokePermission${key}`, permission(key)]);
}
}
});
module.exports = Object.assign(_.fromPairs(lambdas));
function permission(name) {
return {
Type: 'AWS::Lambda::Permission',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: { 'Fn::GetAtt': [name, 'Arn'] },
Principal: 'apigateway.amazonaws.com',
SourceAccount: { Ref: 'AWS::AccountId' },
},
};
}
================================================
FILE: source/templates/master/lex/README.md
================================================
# AWS Lex Template
template for lex resources
================================================
FILE: source/templates/master/lex/bot.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const config = require('./config');
// must change this version to force upgrades to reapply across the entire Bot echo system
const qnabotversion = `${process.env.npm_package_version} - v1`;
module.exports = {
QNAInvokePermission: {
Type: 'AWS::Lambda::Permission',
DependsOn: 'FulfillmentLambdaAliaslive',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: {
'Fn::Join': [':', [
{ 'Fn::GetAtt': ['FulfillmentLambda', 'Arn'] },
'live',
]],
},
Principal: 'lex.amazonaws.com',
SourceAccount: { Ref: 'AWS::AccountId' },
},
},
LexV2Bot: {
Type: 'Custom::LexV2Bot',
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'Lexv2BotLambda',
'Arn',
],
},
description: `QnABot LexV2 Bot${qnabotversion}`,
BuildDate: (new Date()).toISOString(),
localIds: { Ref: 'LexV2BotLocaleIds' },
utterances: config.utterances,
},
},
};
================================================
FILE: source/templates/master/lex/config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
voiceId: 'Joanna',
Clarification: 'Sorry, I did not understand that',
Abort: 'Sorry, I did not understand that',
utterances: require('../../../assets/default-utterances'),
};
================================================
FILE: source/templates/master/lex/fulfillment.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const util = require('../../util');
const examples = _.fromPairs(require('../../examples/outputs')
.names
.map((x) => [x, { 'Fn::GetAtt': ['ExamplesStack', `Outputs.${x}`] }]));
const responsebots = _.fromPairs(require('../../examples/examples/responsebots-lexv2')
.names
.map((x) => [x, { 'Fn::GetAtt': ['ExamplesStack', `Outputs.${x}`] }]));
module.exports = {
Alexa: {
Type: 'AWS::Lambda::Permission',
DependsOn: 'FulfillmentLambdaAliaslive',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: {
'Fn::Join': [':', [
{ 'Fn::GetAtt': ['FulfillmentLambda', 'Arn'] },
'live',
]],
},
Principal: 'alexa-appkit.amazon.com'
},
},
FulfillmentCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/fulfillment.zip' },
BuildDate: (new Date()).toISOString(),
},
},
FulfillmentLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-FulfillmentLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
FulfillmentLambda: {
Type: 'AWS::Lambda::Function',
DependsOn: 'FulfillmentCodeVersion',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/fulfillment.zip' },
S3ObjectVersion: { Ref: 'FulfillmentCodeVersion' },
},
// Note: updates to this lambda function do not automatically generate a new version
// if making changes here, be sure to update FulfillmentLambdaVersionGenerator as appropriate
Environment: {
Variables: {
'Fn::If': [
'BuildExamples',
{
ES_TYPE: { 'Fn::GetAtt': ['Var', 'QnAType'] },
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
LAMBDA_DEFAULT_QUERY: { Ref: 'ESQueryLambda' },
LAMBDA_LOG: { Ref: 'ESLoggingLambda' },
ES_SERVICE_QID: { Ref: 'ESQidLambda' },
ES_SERVICE_PROXY: { Ref: 'ESProxyLambda' },
DYNAMODB_USERSTABLE: { Ref: 'UsersTable' },
DEFAULT_USER_POOL_JWKS_PARAM: { Ref: 'DefaultUserPoolJwksUrl' },
SETTINGS_TABLE: { Ref: 'SettingsTable' },
EMBEDDINGS_API: { Ref: 'EmbeddingsApi' },
EMBEDDINGS_LAMBDA_ARN: { Ref: 'EmbeddingsLambdaArn' },
LLM_API: { Ref: 'LLMApi' },
LLM_LAMBDA_ARN: { Ref: 'LLMLambdaArn' },
AWS_ACCOUNT_ID: { Ref: 'AWS::AccountId' },
DEFAULT_SETTINGS_PARAM: { Ref: 'DefaultQnABotSettings' },
...examples,
...responsebots,
...util.getCommonEnvironmentVariables(),
},
{
ES_TYPE: { 'Fn::GetAtt': ['Var', 'QnAType'] },
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
LAMBDA_DEFAULT_QUERY: { Ref: 'ESQueryLambda' },
LAMBDA_LOG: { Ref: 'ESLoggingLambda' },
ES_SERVICE_QID: { Ref: 'ESQidLambda' },
ES_SERVICE_PROXY: { Ref: 'ESProxyLambda' },
DYNAMODB_USERSTABLE: { Ref: 'UsersTable' },
DEFAULT_USER_POOL_JWKS_PARAM: { Ref: 'DefaultUserPoolJwksUrl' },
SETTINGS_TABLE: { Ref: 'SettingsTable' },
EMBEDDINGS_API: { Ref: 'EmbeddingsApi' },
EMBEDDINGS_LAMBDA_ARN: { Ref: 'EmbeddingsLambdaArn' },
LLM_API: { Ref: 'LLMApi' },
LLM_LAMBDA_ARN: { Ref: 'LLMLambdaArn' },
AWS_ACCOUNT_ID: { Ref: 'AWS::AccountId' },
DEFAULT_SETTINGS_PARAM: { Ref: 'DefaultQnABotSettings' },
...util.getCommonEnvironmentVariables(),
},
],
},
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'FulfillmentLambdaLogGroup' },
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' },
],
MemorySize: 1408,
Role: { 'Fn::GetAtt': ['FulfillmentLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
TracingConfig: {
Mode: {
'Fn::If': [
'XRAYEnabled',
'Active',
'PassThrough',
],
},
},
Tags: [
{
Key: 'Type',
Value: 'Fulfillment',
},
],
VpcConfig: {
'Fn::If': [
'VPCEnabled',
{
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
},
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
cfn_nag: util.cfnNag(['W89', 'W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
FulfillmentLambdaVersionGenerator: {
Type: 'Custom::LambdaVersion',
// this custom resource takes no action on deletes as we keep all versions
// the lambda versions will be deleted along with it's parent Lambda Function
// setting DeletionPolicy of Retain to prevent CFNLambda failures on rollbacks to old versions
DeletionPolicy: 'Retain',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
FunctionName: { Ref: 'FulfillmentLambda' },
Triggers: { // The set of triggers to kick off a Custom Resource Update event
FulfillmentCodeVersionTrigger: [
{ Ref: 'FulfillmentCodeVersion' },
],
LayersTrigger: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' },
],
EmbeddingsTrigger: [
{ Ref: 'EmbeddingsApi' },
{ Ref: 'EmbeddingsLambdaArn' },
],
QASummarizeTrigger: [
{ Ref: 'LLMApi' },
{ Ref: 'LLMLambdaArn' },
],
},
},
},
FulfillmentLambdaAliaslive: {
Type: 'AWS::Lambda::Alias',
DependsOn: 'FulfillmentLambdaVersionGenerator',
Properties: {
FunctionName: { Ref: 'FulfillmentLambda' },
FunctionVersion: { 'Fn::GetAtt': ['FulfillmentLambdaVersionGenerator', 'Version'] },
Name: 'live',
ProvisionedConcurrencyConfig: {
'Fn::If': [
'CreateConcurrency',
{ ProvisionedConcurrentExecutions: { Ref: 'FulfillmentConcurrency' } },
{ Ref: 'AWS::NoValue' },
],
},
},
},
InvokePolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
'Fn::If': [
'BuildExamples',
{
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
'arn:aws:lambda:*:*:function:qna-*',
'arn:aws:lambda:*:*:function:QNA-*',
{ 'Fn::GetAtt': ['ESQueryLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESLoggingLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESQidLambda', 'Arn'] },
{ 'Fn::If': ['EmbeddingsLambdaArn', { Ref: 'EmbeddingsLambdaArn' }, { Ref: 'AWS::NoValue' }] },
{ 'Fn::If': ['LLMLambdaArn', { Ref: 'LLMLambdaArn' }, { Ref: 'AWS::NoValue' }] },
].concat(require('../../examples/outputs').names
.map((x) => ({ 'Fn::GetAtt': ['ExamplesStack', `Outputs.${x}`] }))),
}],
},
{
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
'arn:aws:lambda:*:*:function:qna-*',
'arn:aws:lambda:*:*:function:QNA-*',
{ 'Fn::GetAtt': ['ESQueryLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESLoggingLambda', 'Arn'] },
{ 'Fn::GetAtt': ['ESQidLambda', 'Arn'] },
{ 'Fn::If': ['EmbeddingsLambdaArn', { Ref: 'EmbeddingsLambdaArn' }, { Ref: 'AWS::NoValue' }] },
{ 'Fn::If': ['LLMLambdaArn', { Ref: 'LLMLambdaArn' }, { Ref: 'AWS::NoValue' }] },
],
}],
},
],
},
Roles: [{ Ref: 'FulfillmentLambdaRole' }],
},
},
LexBotPolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'lex:RecognizeText',
],
Resource: [
'arn:aws:lex:*:*:bot:QNA*',
'arn:aws:lex:*:*:bot*',
],
}],
},
Roles: [{ Ref: 'FulfillmentLambdaRole' }],
},
Metadata: {
guard: util.cfnGuard('IAM_POLICY_NON_COMPLIANT_ARN'),
},
},
BedrockInvokeModelAccessPolicyResources: {
Type: 'Custom::ModelAccess',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
EmbeddingsBedrockModelId: {'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'ModelID'] }, { Ref: 'AWS::NoValue' }] },
LLMBedrockModelId: {'Fn::If': ['LLMBedrock', {Ref: 'LLMBedrockModelId'}, { Ref: 'AWS::NoValue' }] },
BedrockKnowledgeBaseModelId: {'Fn::If': ['BedrockKnowledgeBaseEnable', {Ref: 'BedrockKnowledgeBaseModel'}, { Ref: 'AWS::NoValue' }] },
},
},
FulfillmentLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
ManagedPolicyArns: [
{ Ref: 'QueryPolicy' },
],
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
util.translateReadOnly(),
util.comprehendReadOnly(),
util.streamingPermissions(),
{
PolicyName: 'ParamStorePolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'ssm:GetParameter',
'ssm:GetParameters',
],
Resource: [
{
'Fn::Join': [
'', [
'arn:aws:ssm:',
{ 'Fn::Sub': '${AWS::Region}:' },
{ 'Fn::Sub': '${AWS::AccountId}:' },
'parameter/',
{ Ref: 'DefaultUserPoolJwksUrl' },
],
],
},
],
}],
},
},
{
PolicyName: 'DynamoDBPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'dynamodb:GetItem',
'dynamodb:PutItem',
],
Resource: [
{ 'Fn::GetAtt': ['UsersTable', 'Arn'] },
{ 'Fn::GetAtt': ['SettingsTable', 'Arn'] },
],
}],
},
},
{
'Fn::If': [
'BedrockEnable',
{
PolicyName: 'BedrockInvokeModelAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream'
],
Resource: { 'Fn::GetAtt': ['BedrockInvokeModelAccessPolicyResources', 'modelArn'] }
},
{
Sid: 'ApplyGuardrailsToLLMBedrock', // https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions.html#guardrails-permissions-invoke
Effect: 'Allow',
Action: [
'bedrock:ApplyGuardrail',
],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:bedrock:${AWS::Region}:${AWS::AccountId}:guardrail/*' }],
},
],
},
},
{ Ref: 'AWS::NoValue' },
],
},
{
'Fn::If': [
'BedrockKnowledgeBaseEnable',
{
PolicyName: 'BedrockKnowledgeBaseAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'bedrock:Retrieve',
'bedrock:RetrieveAndGenerate',
],
Resource: { 'Fn::Sub': 'arn:${AWS::Partition}:bedrock:${AWS::Region}:${AWS::AccountId}:knowledge-base/${BedrockKnowledgeBaseId}' },
},
{
Sid: 'ApplyGuardrailsToKnowledgeBase', // https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-permissions.html#guardrails-permissions-invoke
Effect: 'Allow',
Action: [
'bedrock:ApplyGuardrail',
],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:bedrock:${AWS::Region}:${AWS::AccountId}:guardrail/*' }],
},
{
Sid: 'GetInferenceProfileForKnowledgeBase',
Effect: 'Allow',
Action: [
'bedrock:GetInferenceProfile',
],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*' }],
},
],
},
},
{ Ref: 'AWS::NoValue' },
],
},
{
PolicyName: 'S3QNABucketReadAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
's3:GetObject',
],
Resource: [
'arn:aws:s3:::QNA*/*',
'arn:aws:s3:::qna*/*',
],
},
],
},
},
{
PolicyName: 'SettingsTableReadAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'dynamodb:Scan',
],
Resource: [{ 'Fn::GetAtt': ['SettingsTable', 'Arn'] }],
},
],
},
}
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
ESWarmerCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/warmer.zip' },
BuildDate: (new Date()).toISOString(),
},
},
ESWarmerLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESWarmerLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESWarmerLambda: {
DependsOn: ['ESWarmerCodeVersion'],
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/warmer.zip' },
S3ObjectVersion: { Ref: 'ESWarmerCodeVersion' },
},
Environment: {
Variables: {
REPEAT_COUNT: '4',
TARGET_PATH: '_search',
TARGET_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
TARGET_URL: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
SETTINGS_TABLE: { Ref: 'SettingsTable' },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.warmer',
LoggingConfig: {
LogGroup: { Ref: 'ESWarmerLambdaLogGroup' },
},
MemorySize: '512',
Role: { 'Fn::GetAtt': ['WarmerLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' },
],
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Warmer',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
WarmerLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'ParamStorePolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Sid: 'AllowES',
Effect: 'Allow',
Action: [
'es:ESHttpGet',
],
Resource: [
'*',
], // these actions cannot be bound to resources other than *
}],
},
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
ESWarmerRule: {
Type: 'AWS::Events::Rule',
Properties: {
ScheduleExpression: 'rate(1 minute)',
Targets: [
{
Id: 'ESWarmerScheduler',
Arn: {
'Fn::GetAtt': [
'ESWarmerLambda',
'Arn',
],
},
},
],
},
},
ESWarmerRuleInvokeLambdaPermission: {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: {
'Fn::GetAtt': [
'ESWarmerLambda',
'Arn',
],
},
Action: 'lambda:InvokeFunction',
Principal: 'events.amazonaws.com',
SourceArn: {
'Fn::GetAtt': [
'ESWarmerRule',
'Arn',
],
},
},
},
};
================================================
FILE: source/templates/master/lex/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = Object.assign(
require('./bot'),
require('./fulfillment'),
);
================================================
FILE: source/templates/master/lex-build/__tests__/poll.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require('aws-sdk-client-mock-jest');
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const { LexModelBuildingServiceClient, GetBotCommand } = require('@aws-sdk/client-lex-model-building-service');
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
const lexClientMock = mockClient(LexModelBuildingServiceClient);
const lambdaClientMock = mockClient(LambdaClient);
const { handler } = require('../poll');
const fs = require('fs');
const context = {};
const callback = (error, response) => {};
const event = {
dummy: 'test',
};
describe('lex poll', () => {
const OLD_ENV = process.env;
beforeEach(() => {
jest.resetModules();
s3ClientMock.reset();
lexClientMock.reset();
lambdaClientMock.reset();
process.env = { ...OLD_ENV };
});
it('updates the lex status in S3 after polling lex', async () => {
const lexStatusFromS3 = {
Body: fs.createReadStream('./master/lex-build/__tests__/test.json'),
};
const lexStatusFromLex = {
status: 'BUILDING',
};
process.env.STATUS_BUCKET = 'test-bucket';
process.env.STATUS_KEY = 'test-key';
process.env.BOT_NAME = 'test-bot';
process.env.AWS_LAMBDA_FUNCTION_NAME = 'test-lambda';
jest
.spyOn(global, 'setTimeout')
.mockImplementation(async (cb) => (typeof cb === 'function' ? cb() : null));
s3ClientMock
.on(GetObjectCommand)
.resolves(lexStatusFromS3)
.on(PutObjectCommand)
.resolves({});
lexClientMock.on(GetBotCommand).resolves(lexStatusFromLex);
lambdaClientMock.on(InvokeCommand).resolves({});
await handler(event, context, callback);
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, GetObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-key',
});
expect(s3ClientMock).toHaveReceivedNthCommandWith(2, PutObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-key',
Body: JSON.stringify({
status: 'BUILDING',
}),
});
expect(lexClientMock).toHaveReceivedCommandWith(GetBotCommand, {
name: process.env.BOT_NAME,
versionOrAlias: '$LATEST',
});
expect(lambdaClientMock).toHaveReceivedCommandWith(InvokeCommand, {
});
});
it('it only invokes lambda if lex in BUILDING state', async () => {
const lexStatusFromS3 = {
Body: fs.createReadStream('./master/lex-build/__tests__/test.json'),
};
const lexStatusFromLex = {
status: 'NOT BUILDING',
};
process.env.STATUS_BUCKET = 'test-bucket';
process.env.STATUS_KEY = 'test-key';
process.env.BOT_NAME = 'test-bot';
process.env.AWS_LAMBDA_FUNCTION_NAME = 'test-lambda';
jest
.spyOn(global, 'setTimeout')
.mockImplementation(async (cb) => (typeof cb === 'function' ? cb() : null));
s3ClientMock
.on(GetObjectCommand)
.resolves(lexStatusFromS3)
.on(PutObjectCommand)
.resolves({});
lexClientMock.on(GetBotCommand).resolves(lexStatusFromLex);
lambdaClientMock.on(InvokeCommand).resolves({});
await handler(event, context, callback);
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, GetObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-key',
});
expect(s3ClientMock).toHaveReceivedNthCommandWith(2, PutObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-key',
Body: JSON.stringify({
status: 'NOT BUILDING',
}),
});
expect(lexClientMock).toHaveReceivedCommandWith(GetBotCommand, {
name: process.env.BOT_NAME,
versionOrAlias: '$LATEST',
});
expect(lambdaClientMock).not.toHaveReceivedCommand(InvokeCommand);
});
it('it handles errors gracefully', async () => {
const lexStatusFromS3 = {
Body: fs.createReadStream('./master/lex-build/__tests__/test.json'),
};
const lexStatusFromLex = {
status: 'BUILDING',
};
process.env.STATUS_BUCKET = 'test-bucket';
process.env.STATUS_KEY = 'test-key';
process.env.BOT_NAME = 'test-bot';
process.env.AWS_LAMBDA_FUNCTION_NAME = 'test-lambda';
jest
.spyOn(global, 'setTimeout')
.mockImplementation(async (cb) => (typeof cb === 'function' ? cb() : null));
s3ClientMock
.on(GetObjectCommand)
.resolves(lexStatusFromS3)
.on(PutObjectCommand)
.resolves({});
lexClientMock.on(GetBotCommand).resolves(lexStatusFromLex);
lambdaClientMock.on(InvokeCommand).rejects('mock rejection');
try {
await handler(event, context, callback);
} catch (e) {
expect(e).toEqual(new Error('mock rejection'));
}
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, GetObjectCommand, {
Bucket: 'test-bucket',
Key: 'test-key',
});
expect(lexClientMock).toHaveReceivedCommandWith(GetBotCommand, {
name: process.env.BOT_NAME,
versionOrAlias: '$LATEST',
});
expect(lambdaClientMock).toHaveReceivedCommand(InvokeCommand);
});
afterAll(() => {
process.env = OLD_ENV;
});
});
================================================
FILE: source/templates/master/lex-build/__tests__/start.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require('aws-sdk-client-mock-jest');
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
const lambdaClientMock = mockClient(LambdaClient);
const crypto = require('crypto');
const { handler } = require('../start');
jest.mock('crypto', () => ({
randomBytes: jest.fn().mockImplementation(() => Buffer.from('', 'utf8')),
}));
describe('lex poll', () => {
const OLD_ENV = process.env;
beforeEach(() => {
jest.resetModules();
s3ClientMock.reset();
lambdaClientMock.reset();
process.env = { ...OLD_ENV };
});
it('initializes lex v2 and updates s3', async () => {
process.env.STATUS_BUCKET = 'test-bucket';
process.env.LEXV2_STATUS_KEY = 'test-status-key';
process.env.BUILD_FUNCTION = 'test-lambda';
s3ClientMock
.on(PutObjectCommand)
.resolves({});
lambdaClientMock.on(InvokeCommand).resolves({});
const result = await handler({}, {});
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, PutObjectCommand, {
Bucket: 'test-bucket',
Key: process.env.LEXV2_STATUS_KEY,
Body: JSON.stringify({
status: 'Starting',
token: '',
}),
});
expect(lambdaClientMock).toHaveReceivedCommandWith(InvokeCommand, {
FunctionName: process.env.BUILD_FUNCTION,
InvocationType: 'Event',
Payload: '{}',
});
expect(result).toEqual({ token: '' });
});
it('only initializes lex v2 if v2 status key not set', async () => {
process.env.STATUS_BUCKET = 'test-bucket';
process.env.STATUS_KEY = '';
process.env.LEXV2_STATUS_KEY = 'test-status-key';
process.env.BUILD_FUNCTION = 'test-lambda';
s3ClientMock
.on(PutObjectCommand)
.resolves({});
lambdaClientMock.on(InvokeCommand).resolves({});
const result = await handler({}, {});
expect(s3ClientMock).toHaveReceivedCommandTimes(PutObjectCommand, 1);
expect(lambdaClientMock).toHaveReceivedCommandWith(InvokeCommand, {
FunctionName: process.env.BUILD_FUNCTION,
InvocationType: 'Event',
Payload: '{}',
});
expect(result).toEqual({ token: '' });
});
afterAll(() => {
process.env = OLD_ENV;
});
});
================================================
FILE: source/templates/master/lex-build/__tests__/test.json
================================================
{
"status": "unknown"
}
================================================
FILE: source/templates/master/lex-build/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
LexBuildLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexBuildLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
LexBuildLambda: lambda({
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/lex-build.zip' },
S3ObjectVersion: { Ref: 'LexBuildCodeVersion' },
}, {
UTTERANCE_BUCKET: { Ref: 'AssetBucket' },
UTTERANCE_KEY: 'default-utterances.json',
POLL_LAMBDA: { 'Fn::GetAtt': ['LexBuildLambdaPoll', 'Arn'] },
STATUS_BUCKET: { Ref: 'BuildStatusBucket' },
LEXV2_STATUS_KEY: 'lexV2status.json',
LEXV2_BUILD_LAMBDA: { Ref: 'Lexv2BotLambda' },
ADDRESS: { 'Fn::Join': ['', ['https://', { 'Fn::GetAtt': ['ESVar', 'ESAddress'] }]] },
INDEX: { 'Fn::GetAtt': ['Var', 'index'] },
...util.getCommonEnvironmentVariables(),
}, process.env.npm_package_config_lambdaRuntime,
{
LogGroup: { Ref: 'LexBuildLambdaLogGroup' },
},
),
LexBuildLambdaStartLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexBuildLambdaStart' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
LexBuildLambdaStart: lambda({
ZipFile: fs.readFileSync(`${__dirname}/start.js`, 'utf8'),
}, {
STATUS_BUCKET: { Ref: 'BuildStatusBucket' },
LEXV2_STATUS_KEY: 'lexV2status.json',
BUILD_FUNCTION: { 'Fn::GetAtt': ['LexBuildLambda', 'Arn'] },
...util.getCommonEnvironmentVariables(),
}, process.env.npm_package_config_lambdaRuntime,
{
LogGroup: { Ref: 'LexBuildLambdaStartLogGroup' },
}),
LexBuildLambdaPollLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexBuildLambdaPoll' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
LexBuildLambdaPoll: lambda({
ZipFile: fs.readFileSync(`${__dirname}/poll.js`, 'utf8'),
}, {
STATUS_BUCKET: { Ref: 'BuildStatusBucket' },
...util.getCommonEnvironmentVariables(),
}, process.env.npm_package_config_lambdaRuntime,
{
LogGroup: { Ref: 'LexBuildLambdaPollLogGroup' },
}),
LexBuildCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/lex-build.zip' },
BuildDate: (new Date()).toISOString(),
},
},
LexBuildInvokePolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
{ 'Fn::GetAtt': ['LexBuildLambda', 'Arn'] },
{ 'Fn::GetAtt': ['LexBuildLambdaPoll', 'Arn'] },
{ 'Fn::GetAtt': ['Lexv2BotLambda', 'Arn'] },
],
}],
},
Roles: [{ Ref: 'LexBuildLambdaRole' }],
},
},
LexBuildLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
util.lexFullAccess(),
{
PolicyName: 'AssetBucketAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['s3:Get*'],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${AssetBucket}*' },
{ 'Fn::Sub': 'arn:aws:s3:::${BuildStatusBucket}*' },
],
}, {
Effect: 'Allow',
Action: ['s3:Put*'],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${BuildStatusBucket}*' },
],
}],
},
}],
Path: '/',
ManagedPolicyArns: [
{ Ref: 'QueryPolicy' },
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12', 'W76', 'F3']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
BuildStatusBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn : ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
Properties: {
LifecycleConfiguration: {
Rules: [{
NoncurrentVersionExpirationInDays: 1,
Status: 'Enabled',
}, {
AbortIncompleteMultipartUpload: {
DaysAfterInitiation: 1,
},
Status: 'Enabled',
}],
},
VersioningConfiguration: {
Status: 'Enabled',
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: {"Fn::Join": ["", [{Ref: 'MainAccessLogBucket'},"/BuildStatus/"]]},
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
},
},
HTTPSOnlyBuildStatusBucketPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'BuildStatusBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'BuildStatusBucket',
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'BuildStatusBucket',
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
Metadata: {
'aws:cdk:path': 'serverless-bot-framework/CloudfrontStaticWebsite/CloudFrontToS3/S3LoggingBucket/Policy/Resource',
},
},
BuildStatusClean: {
Type: 'Custom::S3Clean',
DependsOn: ['CFNInvokePolicy', 'HTTPSOnlyBuildStatusBucketPolicy'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
Bucket: { Ref: 'BuildStatusBucket' },
},
},
};
function lambda(code, variable, runtime, loggingConfig) {
return {
Type: 'AWS::Lambda::Function',
Properties: {
Code: code,
Environment: {
Variables: variable,
},
Handler: 'index.handler',
LoggingConfig: loggingConfig,
MemorySize: '1024',
Role: { 'Fn::GetAtt': ['LexBuildLambdaRole', 'Arn'] },
Runtime: runtime,
Timeout: 900,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
};
}
================================================
FILE: source/templates/master/lex-build/poll.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const { LexModelBuildingServiceClient, GetBotCommand } = require('@aws-sdk/client-lex-model-building-service');
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const lambda = new LambdaClient(customSdkConfig('C001', { region }));
const lex = new LexModelBuildingServiceClient(customSdkConfig('C001', { region }));
const s3 = new S3Client(customSdkConfig('C001', { region }));
const invokeLambda = async function invokeLambda(event) {
return new Promise((res, rej) => {
setTimeout(async () => {
const params = {
FunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
InvocationType: 'Event',
Payload: JSON.stringify(event),
};
const invokeCmd = new InvokeCommand(params);
await lambda.send(invokeCmd)
.then((result) => {
res(result);
})
.catch((e) => {
console.log(e);
rej(e);
});
}, 2000);
});
};
exports.handler = async function (event, context) {
try {
const getObjCmd = new GetObjectCommand({
Bucket: process.env.STATUS_BUCKET,
Key: process.env.STATUS_KEY,
});
const s3Response = await s3.send(getObjCmd);
const readableStream = Buffer.concat(await s3Response.Body.toArray());
const status = JSON.parse(readableStream);
const getBotCmd = new GetBotCommand({
name: process.env.BOT_NAME,
versionOrAlias: '$LATEST',
});
const lexResponse = await lex.send(getBotCmd);
status.status = lexResponse.status;
if (lexResponse.status === 'BUILDING') {
await invokeLambda(event);
}
const params = {
Bucket: process.env.STATUS_BUCKET,
Key: process.env.STATUS_KEY,
Body: JSON.stringify(status),
};
const putObjectCmd = new PutObjectCommand(params);
await s3.send(putObjectCmd);
} catch (error) {
console.log('An error occurred in master lex-build: ', error);
throw new Error(error.message);
}
};
================================================
FILE: source/templates/master/lex-build/start.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const lambda = new LambdaClient(customSdkConfig('C002', { region }));
const s3 = new S3Client(customSdkConfig('C022', { region }));
const crypto = require('crypto');
exports.handler = async function (event, context) {
const token = crypto.randomBytes(16).toString('base64');
const bucket = process.env.STATUS_BUCKET;
const lexV2StatusFile = process.env.LEXV2_STATUS_KEY;
const functionName = process.env.BUILD_FUNCTION;
const body = JSON.stringify({ status: 'Starting', token });
console.log('Initializing ', bucket, lexV2StatusFile);
const params = {
Bucket: bucket,
Key: lexV2StatusFile,
Body: body,
};
const putObjectCmdV2 = new PutObjectCommand(params);
await s3.send(putObjectCmdV2);
// The BUILD_FUNCTION takes care of rebuilding Lex V2 bot
console.log('Invoking ', functionName);
const invokeParams = {
FunctionName: functionName,
InvocationType: 'Event',
Payload: '{}',
};
const invokeCmd = new InvokeCommand(invokeParams);
await lambda.send(invokeCmd);
return { token };
};
================================================
FILE: source/templates/master/lexv2-build/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
LexV2BotLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexV2BotLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
Lexv2BotLambda: lambda({
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/lexv2-build.zip' },
S3ObjectVersion: { Ref: 'Lexv2BotCodeVersion' },
}, {
STACKNAME: { Ref: 'AWS::StackName' },
FULFILLMENT_LAMBDA_ARN: {
'Fn::Join': [':', [
{ 'Fn::GetAtt': ['FulfillmentLambda', 'Arn'] },
'live',
]],
},
LOCALES: { Ref: 'LexV2BotLocaleIds' },
PYTHONPATH: '/var/task/py_modules:/var/runtime:/opt/python',
...util.getCommonEnvironmentVariables(),
}, process.env.npm_package_config_pythonRuntime,
{
LogGroup: { Ref: 'LexV2BotLambdaLogGroup' },
}),
Lexv2BotCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/lexv2-build.zip' },
BuildDate: (new Date()).toISOString(),
},
},
Lexv2BotLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
// There in no LexV2 managed policy (yet) so adding inline policy to allow creation of LexV2 ServiceLinkedRole
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
util.lexFullAccess(),
{
PolicyName: 'LexV2ServiceLinkedRole',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'iam:GetRole',
'iam:DeleteRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
],
},
{
Effect: 'Allow',
Action: [
'iam:CreateServiceLinkedRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
],
Condition: {
StringLike: {
'iam:AWSServiceName': 'lexv2.amazonaws.com',
},
},
},
{
Action: [
'iam:PassRole',
],
Effect: 'Allow',
Resource: [
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
],
Condition: {
StringLike: {
'iam:PassedToService': [
'lexv2.amazonaws.com',
],
},
},
},
{
Action: [
'translate:TranslateText',
'comprehend:DetectDominantLanguage',
],
Effect: 'Allow',
Resource: '*', // these actions cannot be bound to resources other than *
},
],
},
},
{
PolicyName: 'BuildStatusBucketAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['s3:Get*', 's3:Put*'],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${BuildStatusBucket}*' },
],
}],
},
},
],
Path: '/',
ManagedPolicyArns: [
{ Ref: 'QueryPolicy' },
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12', 'W76', 'F3']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
function lambda(code, variable, runtime, loggingConfig) {
return {
Type: 'AWS::Lambda::Function',
Properties: {
Code: code,
Environment: {
Variables: variable,
},
Handler: 'handler.handler',
LoggingConfig: loggingConfig,
MemorySize: '1024',
Role: { 'Fn::GetAtt': ['Lexv2BotLambdaRole', 'Arn'] },
Runtime: runtime,
Timeout: 900,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
};
}
================================================
FILE: source/templates/master/mappings/anonymized-data.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
SolutionHelperAnonymizedData: {
SendAnonymizedData: {
Data: 'Yes',
},
},
};
================================================
FILE: source/templates/master/mappings/bedrock-defaults.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
BedrockDefaults: {
'amazon.titan-embed-text-v1': {
ModelID: 'amazon.titan-embed-text-v1',
MaxTokens: 8000,
EmbeddingsDimensions: 1536,
},
'amazon.titan-embed-text-v2': {
ModelID: 'amazon.titan-embed-text-v2:0',
MaxTokens: 8000,
EmbeddingsDimensions: 1024,
},
'amazon.nova-2-multimodal-embeddings-v1': {
ModelID: 'amazon.nova-2-multimodal-embeddings-v1:0',
MaxTokens: 8172,
EmbeddingsDimensions: 3072,
},
'cohere.embed-english-v3': {
ModelID: 'cohere.embed-english-v3',
MaxTokens: 512,
EmbeddingsDimensions: 1024,
},
'cohere.embed-multilingual-v3': {
ModelID: 'cohere.embed-multilingual-v3',
MaxTokens: 512,
EmbeddingsDimensions: 1024,
},
'global.cohere.embed-v4': {
ModelID: 'global.cohere.embed-v4:0',
MaxTokens: 128000,
EmbeddingsDimensions: 1536,
},
},
};
================================================
FILE: source/templates/master/opensearch/README.md
================================================
# OpenSearch Domain Template
Template for opensearch cluster
================================================
FILE: source/templates/master/opensearch/__tests__/handler.fixtures.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.event = {
RequestType: 'Create',
ResponseURL: 'https://localhost',
ResourceProperties: {
name: 'test',
},
};
exports.endMock = jest.fn();
exports.writeMock = jest.fn().mockImplementation((body) => {
expect(JSON.parse(body).PhysicalResourceId).toEqual('mock log stream name');
});
exports.doneMock = jest.fn();
================================================
FILE: source/templates/master/opensearch/__tests__/handler.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require("aws-sdk-client-mock-jest");
const { EventEmitter } = require('events');
const httpsMock = require('https');
const Stream = require('stream');
const { OpenSearchClient, DescribeDomainCommand } = require('@aws-sdk/client-opensearch');
const { mockClient } = require('aws-sdk-client-mock');
const esMock = mockClient(OpenSearchClient);
const { handler } = require('../handler');
const { event, endMock, writeMock, doneMock } = require('./handler.fixtures');
const context = {
logStreamName: 'mock log stream name',
done: doneMock,
};
const emitter = new EventEmitter();
emitter.write = writeMock;
emitter.end = endMock;
describe('bootstrap handler', () => {
beforeEach(() => {
jest.resetModules();
endMock.mockRestore();
doneMock.mockRestore();
writeMock.mockRestore();
esMock.reset();
});
it('should send a put request to the provided url', async () => {
const message = new Stream();
esMock.on(DescribeDomainCommand)
.resolvesOnce({
DomainStatus: {
DomainId: '123456789012/cli-example',
DomainName: 'cli-example',
ARN: 'arn:aws:es:us-east-1:123456789012:domain/cli-example',
Created: true,
Deleted: false,
Endpoint: 'search-cli-example-1a2a3a4a5a6a7a8a9a0a.us-east-1.es.amazonaws.com',
},
});
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
expect(esMock).toHaveReceivedCommandWith(DescribeDomainCommand, { DomainName: "test" });
expect(writeMock).toHaveBeenCalledWith("{\"Status\":\"SUCCESS\",\"Reason\":\"See the details in CloudWatch Log Stream: mock log stream name\",\"PhysicalResourceId\":\"mock log stream name\",\"NoEcho\":false,\"Data\":{\"Name\":\"cli-example\",\"Arn\":\"arn:aws:es:us-east-1:123456789012:domain/cli-example\"}}");
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should handle errors from describe domain gracefully', async () => {
const message = new Stream();
esMock.rejects('mocked rejection');
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
expect(esMock).toHaveReceivedCommandWith(DescribeDomainCommand, { DomainName: "test" });
expect(writeMock).toHaveBeenCalledWith("{\"Status\":\"FAILED\",\"Reason\":\"See the details in CloudWatch Log Stream: mock log stream name\",\"PhysicalResourceId\":\"mock log stream name\",\"NoEcho\":false}");
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should handle cfn response errors and close context', async () => {
const message = new Stream();
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(event, context);
emitter.emit('error', 'error message');
expect(esMock).toHaveReceivedCommandWith(DescribeDomainCommand, { DomainName: "test" });
expect(writeMock).toHaveBeenCalledWith("{\"Status\":\"FAILED\",\"Reason\":\"See the details in CloudWatch Log Stream: mock log stream name\",\"PhysicalResourceId\":\"mock log stream name\",\"NoEcho\":false}");
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
it('should respond to delete requests', async () => {
const message = new Stream();
const clonedEvent = JSON.parse(JSON.stringify(event));
clonedEvent.RequestType = 'Delete';
httpsMock.request = jest.fn().mockImplementation((options, cb) => {
cb(message);
expect(options.hostname).toEqual('localhost');
expect(options.method).toEqual('PUT');
expect(options.port).toEqual(443);
message.emit('end');
return emitter;
});
await handler(clonedEvent, context);
emitter.emit('end', 'end');
expect(writeMock).toHaveBeenCalled();
expect(endMock).toHaveBeenCalled();
expect(doneMock).toHaveBeenCalled();
});
});
================================================
FILE: source/templates/master/opensearch/es.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
const properties = {
CognitoOptions: {
Enabled: true,
IdentityPoolId: { Ref: 'OpenSearchDashboardsIdPool' },
RoleArn: { 'Fn::GetAtt': ['ESCognitoRole', 'Arn'] },
UserPoolId: { Ref: 'UserPool' },
},
EBSOptions: {
EBSEnabled: true,
VolumeSize: { Ref: 'OpenSearchEBSVolumeSize' },
VolumeType: 'gp3',
},
EngineVersion: 'OpenSearch_2.19',
SnapshotOptions: {
AutomatedSnapshotStartHour: '0',
},
AdvancedOptions: {
'rest.action.multi.allow_explicit_index': 'true',
},
EncryptionAtRestOptions: {
Enabled: true,
},
NodeToNodeEncryptionOptions: {
Enabled: true,
},
DomainEndpointOptions: {
EnforceHTTPS: true,
TLSSecurityPolicy: 'Policy-Min-TLS-1-2-2019-07',
},
VPCOptions: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
};
const domainConfigWithMasterNodes = {
...properties,
ClusterConfig: {
DedicatedMasterEnabled: 'true',
DedicatedMasterType: { Ref: 'OpenSearchMasterNodeInstanceType' },
DedicatedMasterCount: { Ref: 'OpenSearchMasterNodeCount' },
InstanceCount: { Ref: 'OpenSearchNodeCount' },
InstanceType: { Ref: 'OpenSearchNodeInstanceType' },
ZoneAwarenessEnabled: { 'Fn::If': ['SingleNode', false, true] },
},
};
const domainConfigWithoutMasterNodes = {
...properties,
ClusterConfig: {
DedicatedMasterEnabled: 'false',
InstanceCount: { Ref: 'OpenSearchNodeCount' },
InstanceType: { Ref: 'OpenSearchNodeInstanceType' },
ZoneAwarenessEnabled: { 'Fn::If': ['SingleNode', false, true] },
},
};
module.exports = {
OpensearchDomain: {
Type: 'AWS::OpenSearchService::Domain',
DependsOn: ['PreUpgradeExport', 'ESCognitoRole'],
Condition: 'CreateDomain',
UpdatePolicy: {
EnableVersionUpgrade: true,
},
Metadata: {
checkov: {
skip: [
{
id: 'CKV_AWS_84',
comment: 'Logging is enabled via custom resource - see source/templates/master/opensearch/updates.js',
},
{
id: 'CKV_AWS_317',
comment: 'Logging is enabled via custom resource - see source/templates/master/opensearch/updates.js',
},
],
},
},
Properties: { 'Fn::If': ['MasterNodesEnabled', domainConfigWithMasterNodes, domainConfigWithoutMasterNodes] },
},
ESCognitoRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'opensearchservice.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.esCognitoAccess(),
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12', 'F38']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
================================================
FILE: source/templates/master/opensearch/firehose.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
FeedbackKinesisFirehoseLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/kinesisfirehose/${AWS::StackName}-FeedbackKinesisFirehose' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
cfn_nag: {
rules_to_suppress: [
{
id: 'W86',
reason: 'LogGroup is encrypted by default.',
},
],
},
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
FeedbackKinesisFirehoseStreamOpenSearch: {
Type: 'AWS::Logs::LogStream',
DependsOn: ['FeedbackKinesisFirehoseLogGroup'],
Properties: {
LogGroupName: { Ref: 'FeedbackKinesisFirehoseLogGroup' },
LogStreamName: 'OpenSearchDestinationDelivery',
},
},
FeedbackKinesisFirehoseStreamS3: {
Type: 'AWS::Logs::LogStream',
DependsOn: ['FeedbackKinesisFirehoseLogGroup'],
Properties: {
LogGroupName: { Ref: 'FeedbackKinesisFirehoseLogGroup' },
LogStreamName: 'S3BackupDelivery',
},
},
FeedbackKinesisFirehose: {
Type: 'AWS::KinesisFirehose::DeliveryStream',
Metadata: {
guard: util.cfnGuard(
'KINESIS_FIREHOSE_SPLUNK_DESTINATION_CONFIGURATION_NO_PLAINTEXT_PASSWORD',
'KINESIS_FIREHOSE_REDSHIFT_DESTINATION_CONFIGURATION_NO_PLAINTEXT_PASSWORD',
),
},
DependsOn: [ 'FeedbackKinesisFirehoseStreamS3', 'FeedbackKinesisFirehoseStreamOpenSearch', 'FirehoseESS3Role'],
Properties: {
DeliveryStreamType: 'DirectPut',
DeliveryStreamEncryptionConfigurationInput: {
KeyType: 'AWS_OWNED_CMK',
},
AmazonopensearchserviceDestinationConfiguration: {
BufferingHints: {
IntervalInSeconds: 60,
SizeInMBs: 5,
},
CloudWatchLoggingOptions: {
Enabled: true,
LogGroupName: { Ref: 'FeedbackKinesisFirehoseLogGroup' },
LogStreamName: { Ref: 'FeedbackKinesisFirehoseStreamOpenSearch' },
},
DomainARN: { 'Fn::GetAtt': ['ESVar', 'ESArn'] },
IndexName: { 'Fn::Sub': '${Var.FeedbackIndex}' },
IndexRotationPeriod: 'NoRotation',
RetryOptions: {
DurationInSeconds: 300,
},
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
S3BackupMode: 'AllDocuments',
S3Configuration: {
BucketARN: { 'Fn::GetAtt': ['MetricsBucket', 'Arn'] },
CloudWatchLoggingOptions: {
Enabled: true,
LogGroupName: { Ref: 'FeedbackKinesisFirehoseLogGroup' },
LogStreamName: { Ref: 'FeedbackKinesisFirehoseStreamS3' },
},
BufferingHints: {
IntervalInSeconds: 60,
SizeInMBs: 5,
},
Prefix: 'feedback/',
CompressionFormat: 'UNCOMPRESSED',
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
},
TypeName: '',
VpcConfiguration: {
'Fn::If': [
'VPCEnabled',
{
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
},
{ Ref: 'AWS::NoValue' },
],
},
},
},
},
GeneralKinesisFirehoseLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/kinesisfirehose/${AWS::StackName}-GeneralKinesisFirehose' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
cfn_nag: {
rules_to_suppress: [
{
id: 'W86',
reason: 'LogGroup is encrypted by default.',
},
],
},
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
GeneralKinesisFirehoseStreamOpenSearch: {
Type: 'AWS::Logs::LogStream',
Properties: {
LogGroupName: { Ref: 'GeneralKinesisFirehoseLogGroup' },
LogStreamName: 'OpenSearchDestinationDelivery',
},
},
GeneralKinesisFirehoseStreamS3: {
Type: 'AWS::Logs::LogStream',
Properties: {
LogGroupName: { Ref: 'GeneralKinesisFirehoseLogGroup' },
LogStreamName: 'S3BackupDelivery',
},
},
GeneralKinesisFirehose: {
Type: 'AWS::KinesisFirehose::DeliveryStream',
Metadata: {
guard: util.cfnGuard(
'KINESIS_FIREHOSE_REDSHIFT_DESTINATION_CONFIGURATION_NO_PLAINTEXT_PASSWORD',
'KINESIS_FIREHOSE_SPLUNK_DESTINATION_CONFIGURATION_NO_PLAINTEXT_PASSWORD',
),
},
DependsOn: ['GeneralKinesisFirehoseStreamOpenSearch', 'GeneralKinesisFirehoseStreamS3', 'FirehoseESS3Role'],
Properties: {
DeliveryStreamType: 'DirectPut',
DeliveryStreamEncryptionConfigurationInput: {
KeyType: 'AWS_OWNED_CMK',
},
AmazonopensearchserviceDestinationConfiguration: {
BufferingHints: {
IntervalInSeconds: 60,
SizeInMBs: 5,
},
CloudWatchLoggingOptions: {
Enabled: true,
LogGroupName: { Ref: 'GeneralKinesisFirehoseLogGroup' },
LogStreamName: { Ref: 'GeneralKinesisFirehoseStreamOpenSearch' },
},
DomainARN: { 'Fn::GetAtt': ['ESVar', 'ESArn'] },
IndexName: { 'Fn::Sub': '${Var.MetricsIndex}' },
IndexRotationPeriod: 'NoRotation',
RetryOptions: {
DurationInSeconds: 300,
},
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
S3BackupMode: 'AllDocuments',
S3Configuration: {
BucketARN: { 'Fn::GetAtt': ['MetricsBucket', 'Arn'] },
CloudWatchLoggingOptions: {
Enabled: true,
LogGroupName: { Ref: 'GeneralKinesisFirehoseLogGroup' },
LogStreamName: { Ref: 'GeneralKinesisFirehoseStreamS3' },
},
Prefix: 'metrics/',
BufferingHints: {
IntervalInSeconds: 60,
SizeInMBs: 5,
},
CompressionFormat: 'UNCOMPRESSED',
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
},
TypeName: '',
VpcConfiguration: {
'Fn::If': [
'VPCEnabled',
{
RoleARN: { 'Fn::GetAtt': ['FirehoseESS3Role', 'Arn'] },
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
},
{ Ref: 'AWS::NoValue' },
],
},
},
},
},
MetricsBucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn: ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
DeletionPolicy: 'Delete',
Properties: {
VersioningConfiguration: {
Status: 'Enabled',
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: { 'Fn::Join': ['', [{ Ref: 'MainAccessLogBucket' }, '/Metrics/']] },
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
Tags: [
{
Key: 'Use',
Value: 'Metrics',
},
],
},
},
HTTPSOnlyMetricBucketsPolicy: {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: 'MetricsBucket',
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': ['MetricsBucket', 'Arn'],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': ['MetricsBucket', 'Arn'],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
},
MetricsBucketClean: {
Type: 'Custom::S3Clean',
DependsOn: ['CFNInvokePolicy', 'HTTPSOnlyMetricBucketsPolicy'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
Bucket: { Ref: 'MetricsBucket' },
},
},
FirehoseESS3Role: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'firehose.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
{
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Sid: 'FirehoseS3DeliveryPermissions',
Effect: 'Allow',
Action: [
's3:AbortMultipartUpload',
's3:GetBucketLocation',
's3:GetObject',
's3:ListBucket',
's3:ListBucketMultipartUploads',
's3:PutObject',
],
Resource: [
{ 'Fn::GetAtt': ['MetricsBucket', 'Arn'] },
{ 'Fn::Join': ['', [{ 'Fn::GetAtt': ['MetricsBucket', 'Arn'] }, '/*']] },
],
},
{
Sid: 'FirehoseLambdaPermissions',
Effect: 'Allow',
Action: ['lambda:InvokeFunction', 'lambda:GetFunctionConfiguration'],
Resource: [
{
'Fn::Join': [
'',
[
'arn:aws:lambda:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%',
],
],
},
],
},
{
Sid: 'FirehoseOpenSearchDestinationPermissions',
Effect: 'Allow',
Action: [
'es:DescribeDomain',
'es:DescribeDomains',
'es:DescribeDomainConfig',
'es:ESHttpPost',
'es:ESHttpPut',
'es:ESHttpGet',
],
Resource: [
{ 'Fn::GetAtt': ['ESVar', 'ESArn'] },
{ 'Fn::Join': ['', [{ 'Fn::GetAtt': ['ESVar', 'ESArn'] }, '/*']] },
],
},
{
Sid: 'FirehoseLogsPermissions',
Effect: 'Allow',
Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
Resource: [
{
'Fn::Join': [
'',
[
'arn:aws:logs:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':log-group:/aws/kinesisfirehose/*',
],
],
},
],
},
{
Sid: 'FireHoseVPCConfiguration', // https://docs.aws.amazon.com/firehose/latest/APIReference/API_VpcConfigurationDescription.html
Effect: 'Allow',
Action: [
'ec2:DescribeVpcs',
'ec2:DescribeVpcAttribute',
'ec2:DescribeSubnets',
'ec2:DescribeSecurityGroups',
'ec2:DescribeNetworkInterfaces',
'ec2:CreateNetworkInterface',
'ec2:CreateNetworkInterfacePermission',
'ec2:DeleteNetworkInterface',
],
Resource: '*', // these actions cannot be bound to resources other than *
},
],
},
PolicyName: 'QnAFirehose',
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
================================================
FILE: source/templates/master/opensearch/handler.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { OpenSearchClient, DescribeDomainCommand } = require('@aws-sdk/client-opensearch');
const region = process.env.AWS_REGION;
const client = new OpenSearchClient({
customUserAgent: [
[`AWSSOLUTION/${process.env.SOLUTION_ID}/${process.env.SOLUTION_VERSION}`],
[`AWSSOLUTION-CAPABILITY/${process.env.SOLUTION_ID}-C023/${process.env.SOLUTION_VERSION}`]
],
region,
});
const SUCCESS = 'SUCCESS';
const FAILED = 'FAILED';
const https = require('https');
const { URL } = require('url');
async function send(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
return new Promise((resolve, reject) => {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData,
});
console.log('Response body:\n', responseBody);
const parsedUrl = new URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname + parsedUrl.search,
method: 'PUT',
headers: {
'content-type': '',
'content-length': responseBody.length,
},
};
const request = https.request(options, (response) => {
console.log(`Status code: ${response.statusCode}`);
console.log(`Status message: ${response.statusMessage}`);
response.on('end', () => {
resolve();
});
});
request.on('error', (error) => {
console.log(`send(..) failed executing https.request(..): ${error}`);
reject(error);
});
request.write(responseBody);
request.end();
});
}
exports.handler = async function (event, context) {
console.log(JSON.stringify(event, null, 2));
if (event.RequestType !== 'Delete') {
const describeDomainCmd = new DescribeDomainCommand({
DomainName: event.ResourceProperties.name,
});
try {
const info = await client.send(describeDomainCmd);
await send(event, context, SUCCESS, {
Name: info.DomainStatus.DomainName,
Arn: info.DomainStatus.ARN,
Endpoint: info.DomainStatus.Endpoints,
});
} catch (e) {
console.log(e);
await send(event, context, FAILED);
}
} else {
await send(event, context, SUCCESS);
}
context.done();
};
================================================
FILE: source/templates/master/opensearch/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = Object.assign(
require('./es'),
require('./info'),
require('./firehose'),
require('./proxy'),
require('./updates')
);
================================================
FILE: source/templates/master/opensearch/index_mappings.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
properties: {
// all doc types have qid
qid: {
type: 'keyword',
},
// 'qna' doc type fields
quniqueterms: {
type: 'text',
analyzer: '${Language}' === 'English' ? 'rebuilt_english_unique' : 'rebuilt_${Language}',
},
questions: {
type: 'nested',
properties: {
q: {
type: 'text',
analyzer: 'rebuilt_${Language}',
},
q_vector: {
type: 'knn_vector',
dimension: '${EmbeddingsDimensions}',
method: {
name: 'hnsw',
space_type: 'cosinesimil',
engine: 'nmslib',
},
},
},
},
a: {
type: 'text',
analyzer: 'rebuilt_${Language}',
},
a_vector: {
type: 'knn_vector',
dimension: '${EmbeddingsDimensions}',
method: {
name: 'hnsw',
space_type: 'cosinesimil',
engine: 'nmslib',
},
},
t: {
type: 'text', analyzer: 'whitespace',
},
r: {
properties: {
imageUrl: { type: 'keyword' },
title: { type: 'text' },
},
},
l: {
type: 'keyword',
},
// 'text' doc type fields
passage: {
type: 'text', analyzer: 'rebuilt_${Language}',
},
passage_vector: {
type: 'knn_vector',
dimension: '${EmbeddingsDimensions}',
method: {
name: 'hnsw',
space_type: 'cosinesimil',
engine: 'nmslib',
},
},
// 'quiz' doc type fields
question: {
type: 'text',
analyzer: 'rebuilt_${Language}',
},
incorrectAnswers: {
type: 'text',
analyzer: 'rebuilt_${Language}',
},
correctAnswers: {
type: 'text',
analyzer: 'rebuilt_${Language}',
},
},
};
================================================
FILE: source/templates/master/opensearch/index_settings.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
number_of_shards: '1',
'index.knn': true,
analysis: {
filter: {
english_stop: {
type: "stop",
stopwords: "_english_"
},
english_keywords: {
type: "keyword_marker",
keywords: ["example"]
},
english_stemmer: {
type: "stemmer",
language: "english"
},
english_possessive_stemmer: {
type: "stemmer",
language: "possessive_english"
},
arabic_stop: {
type: "stop",
stopwords: "_arabic_"
},
arabic_stemmer: {
type: "stemmer",
language: "arabic"
},
arabic_keywords: {
type: "keyword_marker",
keywords: ["مثال"]
},
armenian_stop: {
type: "stop",
stopwords: "_armenian_"
},
armenian_keywords: {
type: "keyword_marker",
keywords: ["օրինակ"]
},
armenian_stemmer: {
type: "stemmer",
language: "armenian"
},
basque_stop: {
type: "stop",
stopwords: "_basque_"
},
basque_keywords: {
type: "keyword_marker",
keywords: ["Adibidez"]
},
basque_stemmer: {
type: "stemmer",
language: "basque"
},
bengali_stop: {
type: "stop",
stopwords: "_bengali_"
},
bengali_keywords: {
type: "keyword_marker",
keywords: ["উদাহরণ"]
},
bengali_stemmer: {
type: "stemmer",
language: "bengali"
},
brazilian_stop: {
type: "stop",
stopwords: "_brazilian_"
},
brazilian_keywords: {
type: "keyword_marker",
keywords: ["exemplo"]
},
brazilian_stemmer: {
type: "stemmer",
language: "brazilian"
},
bulgarian_stop: {
type: "stop",
stopwords: "_bulgarian_"
},
bulgarian_keywords: {
type: "keyword_marker",
keywords: ["пример"]
},
bulgarian_stemmer: {
type: "stemmer",
language: "bulgarian"
},
catalan_elision: {
type: "elision",
articles_case: true,
articles: ["d", "l", "m", "n", "s", "t"]
},
catalan_stop: {
type: "stop",
stopwords: "_catalan_"
},
catalan_keywords: {
type: "keyword_marker",
keywords: ["example"]
},
catalan_stemmer: {
type: "stemmer",
language: "catalan"
},
czech_stop: {
type: "stop",
stopwords: "_czech_"
},
czech_keywords: {
type: "keyword_marker",
keywords: ["příklad"]
},
czech_stemmer: {
type: "stemmer",
language: "czech"
},
danish_stop: {
type: "stop",
stopwords: "_danish_"
},
danish_keywords: {
type: "keyword_marker",
keywords: ["eksempel"]
},
danish_stemmer: {
type: "stemmer",
language: "danish"
},
dutch_stop: {
type: "stop",
stopwords: "_dutch_"
},
dutch_stemmer: {
type: "stemmer",
language: "dutch"
},
dutch_keywords: {
type: "keyword_marker",
keywords: ["voorbeeld"]
},
dutch_override: {
type: "stemmer_override",
rules: ["fiets=>fiets", "bromfiets=>bromfiets", "ei=>eier", "kind=>kinder"]
},
estonian_stop: {
type: "stop",
stopwords: "_estonian_"
},
estonian_keywords: {
type: "keyword_marker",
keywords: ["näide"]
},
estonian_stemmer: {
type: "stemmer",
language: "estonian"
},
finnish_stop: {
type: "stop",
stopwords: "_finnish_"
},
finnish_stemmer: {
type: "stemmer",
language: "finnish"
},
finnish_keywords: {
type: "keyword_marker",
keywords: ["esimerkki"]
},
french_elision: {
type: "elision",
articles_case: true,
articles: ["l", "m", "t", "qu", "n", "s", "j", "d", "c",
"jusqu", "quoiqu", "lorsqu", "puisqu"]
},
french_stop: {
type: "stop",
stopwords: "_french_"
},
french_keywords: {
type: "keyword_marker",
keywords: ["Example"]
},
french_stemmer: {
type: "stemmer",
language: "light_french"
},
galician_stop: {
type: "stop",
stopwords: "_galician_"
},
galician_keywords: {
type: "keyword_marker",
keywords: ["exemplo"]
},
galician_stemmer: {
type: "stemmer",
language: "galician"
},
german_stop: {
type: "stop",
stopwords: "_german_"
},
german_stemmer: {
type: "stemmer",
language: "light_german"
},
german_keywords: {
type: "keyword_marker",
keywords: ["Beispiel"]
},
greek_stop: {
type: "stop",
stopwords: "_greek_"
},
greek_lowercase: {
type: "lowercase",
language: "greek"
},
greek_keywords: {
type: "keyword_marker",
keywords: ["παράδειγμα"]
},
greek_stemmer: {
type: "stemmer",
language: "greek"
},
hindi_stop: {
type: "stop",
stopwords: "_hindi_"
},
hindi_stemmer: {
type: "stemmer",
language: "hindi"
},
hindi_keywords: {
type: "keyword_marker",
keywords: ["उदाहरण"]
},
hungarian_stop: {
type: "stop",
stopwords: "_hungarian_"
},
hungarian_keywords: {
type: "keyword_marker",
keywords: ["példa"]
},
hungarian_stemmer: {
type: "stemmer",
language: "hungarian"
},
indonesian_stop: {
type: "stop",
stopwords: "_indonesian_"
},
indonesian_keywords: {
type: "keyword_marker",
keywords: ["contoh"]
},
indonesian_stemmer: {
type: "stemmer",
language: "indonesian"
},
irish_hyphenation: {
type: "stop",
stopwords: ["h", "n", "t"],
ignore_case: true
},
irish_elision: {
type: "elision",
articles: ["d", "m", "b"],
articles_case: true
},
irish_stop: {
type: "stop",
stopwords: "_irish_"
},
irish_keywords: {
type: "keyword_marker",
keywords: ["sampla"]
},
irish_lowercase: {
type: "lowercase",
language: "irish"
},
irish_stemmer: {
type: "stemmer",
language: "irish"
},
italian_elision: {
type: "elision",
articles: ["c", "l", "all", "dall", "dell", "nell", "sull", "coll", "pell", "gl",
"agl", "dagl", "degl", "negl", "sugl", "un", "m", "t", "s", "v", "d"],
articles_case: true
},
italian_stop: {
type: "stop",
stopwords: "_italian_"
},
italian_stemmer: {
type: "stemmer",
language: "light_italian"
},
italian_keywords: {
type: "keyword_marker",
keywords: ["esempio"]
},
latvian_stop: {
type: "stop",
stopwords: "_latvian_"
},
latvian_keywords: {
type: "keyword_marker",
keywords: ["piemērs"]
},
latvian_stemmer: {
type: "stemmer",
language: "latvian"
},
lithuanian_stop: {
type: "stop",
stopwords: "_lithuanian_"
},
lithuanian_keywords: {
type: "keyword_marker",
keywords: ["pavyzdys"]
},
lithuanian_stemmer: {
type: "stemmer",
language: "lithuanian"
},
norwegian_stop: {
type: "stop",
stopwords: "_norwegian_"
},
norwegian_keywords: {
type: "keyword_marker",
keywords: ["eksempel"]
},
norwegian_stemmer: {
type: "stemmer",
language: "norwegian"
},
portuguese_stop: {
type: "stop",
stopwords: "_portuguese_"
},
portuguese_keywords: {
type: "keyword_marker",
keywords: ["exemplo"]
},
portuguese_stemmer: {
type: "stemmer",
language: "light_portuguese"
},
romanian_stop: {
type: "stop",
stopwords: "_romanian_"
},
romanian_keywords: {
type: "keyword_marker",
keywords: ["exemplu"]
},
romanian_stemmer: {
type: "stemmer",
language: "romanian"
},
russian_stop: {
type: "stop",
stopwords: "_russian_"
},
russian_stemmer: {
type: "stemmer",
language: "russian"
},
russian_keywords: {
type: "keyword_marker",
keywords: ["пример"]
},
sorani_stop: {
type: "stop",
stopwords: "_sorani_"
},
sorani_keywords: {
type: "keyword_marker",
keywords: ["mînak"]
},
sorani_stemmer: {
type: "stemmer",
language: "sorani"
},
spanish_stop: {
type: "stop",
stopwords: "_spanish_"
},
spanish_stemmer: {
type: "stemmer",
language: "light_spanish"
},
spanish_keywords: {
type: "keyword_marker",
keywords: ["ejemplo"]
},
swedish_stop: {
type: "stop",
stopwords: "_swedish_"
},
swedish_keywords: {
type: "keyword_marker",
keywords: ["exempel"]
},
swedish_stemmer: {
type: "stemmer",
language: "swedish"
},
turkish_stop: {
type: "stop",
stopwords: "_turkish_"
},
turkish_lowercase: {
type: "lowercase",
language: "turkish"
},
turkish_keywords: {
type: "keyword_marker",
keywords: ["örnek"]
},
turkish_stemmer: {
type: "stemmer",
language: "turkish"
},
thai_stop: {
type: "stop",
stopwords: "_thai_"
}
},
analyzer: {
rebuilt_English: {
type: "custom",
tokenizer: "standard",
filter: ["english_possessive_stemmer", "lowercase", "english_stop",
"english_keywords", "english_stemmer"],
},
rebuilt_English_unique: {
type: "custom",
tokenizer: "standard",
filter: ["english_possessive_stemmer", "lowercase", "english_stop",
"english_keywords", "english_stemmer", "unique"],
},
rebuilt_Arabic: {
tokenizer: "standard",
filter: ["lowercase", "decimal_digit", "arabic_stop", "arabic_normalization",
"arabic_keywords", "arabic_stemmer"],
},
rebuilt_Armenian: {
tokenizer: "standard",
filter: ["lowercase", "armenian_stop", "armenian_keywords", "armenian_stemmer"],
},
rebuilt_Basque: {
tokenizer: "standard",
filter: ["lowercase", "basque_stop", "basque_keywords", "basque_stemmer"],
},
rebuilt_Bengali: {
tokenizer: "standard",
filter: ["lowercase", "decimal_digit", "bengali_keywords", "indic_normalization",
"bengali_normalization", "bengali_stop", "bengali_stemmer"],
},
rebuilt_Brazilian: {
tokenizer: "standard",
filter: ["lowercase", "brazilian_stop", "brazilian_keywords", "brazilian_stemmer"],
},
rebuilt_Bulgarian: {
tokenizer: "standard",
filter: ["lowercase", "bulgarian_stop", "bulgarian_keywords", "bulgarian_stemmer"],
},
rebuilt_Catalan: {
tokenizer: "standard",
filter: ["catalan_elision", "lowercase", "catalan_stop", "catalan_keywords", "catalan_stemmer"],
},
rebuilt_Czech: {
tokenizer: "standard",
filter: ["lowercase", "czech_stop", "czech_keywords", "czech_stemmer"],
},
rebuilt_Danish: {
tokenizer: "standard",
filter: ["lowercase", "danish_stop", "danish_keywords", "danish_stemmer"],
},
rebuilt_Dutch: {
tokenizer: "standard",
filter: ["lowercase", "dutch_stop", "dutch_keywords", "dutch_override", "dutch_stemmer"],
},
rebuilt_Estonian: {
tokenizer: "standard",
filter: ["lowercase", "estonian_stop", "estonian_keywords", "estonian_stemmer"],
},
rebuilt_Finnish: {
tokenizer: "standard",
filter: ["lowercase", "finnish_stop", "finnish_keywords", "finnish_stemmer"],
},
rebuilt_French: {
tokenizer: "standard",
filter: ["french_elision", "lowercase", "french_stop", "french_keywords", "french_stemmer"],
},
rebuilt_Galician: {
tokenizer: "standard",
filter: ["lowercase", "galician_stop", "galician_keywords", "galician_stemmer"],
},
rebuilt_German: {
tokenizer: "standard",
filter: ["lowercase", "german_stop", "german_keywords", "german_normalization", "german_stemmer"],
},
rebuilt_Greek: {
tokenizer: "standard",
filter: ["greek_lowercase", "greek_stop", "greek_keywords", "greek_stemmer"],
},
rebuilt_Hindi: {
tokenizer: "standard",
filter: ["lowercase", "decimal_digit", "hindi_keywords", "indic_normalization",
"hindi_normalization", "hindi_stop", "hindi_stemmer"],
},
rebuilt_Hungarian: {
tokenizer: "standard",
filter: ["lowercase", "hungarian_stop", "hungarian_keywords", "hungarian_stemmer"],
},
rebuilt_Indonesian: {
tokenizer: "standard",
filter: ["lowercase", "indonesian_stop", "indonesian_keywords", "indonesian_stemmer"],
},
rebuilt_Irish: {
tokenizer: "standard",
filter: ["irish_hyphenation", "irish_elision", "irish_lowercase", "irish_stop",
"irish_keywords", "irish_stemmer"],
},
rebuilt_Italian: {
tokenizer: "standard",
filter:["italian_elision","lowercase","italian_stop","italian_keywords","italian_stemmer"],
},
rebuilt_Latvian: {
tokenizer: "standard",
filter: ["lowercase", "latvian_stop", "latvian_keywords", "latvian_stemmer"],
},
rebuilt_Lithuanian: {
tokenizer: "standard",
filter: ["lowercase", "lithuanian_stop", "lithuanian_keywords", "lithuanian_stemmer"],
},
rebuilt_Norwegian: {
tokenizer: "standard",
filter: ["lowercase", "norwegian_stop", "norwegian_keywords", "norwegian_stemmer"],
},
rebuilt_Portuguese: {
tokenizer: "standard",
filter: ["lowercase", "portuguese_stop", "portuguese_keywords", "portuguese_stemmer"],
},
rebuilt_Romanian: {
tokenizer: "standard",
filter: ["lowercase", "romanian_stop", "romanian_keywords", "romanian_stemmer"],
},
rebuilt_Russian: {
tokenizer: "standard",
filter: ["lowercase", "russian_stop", "russian_keywords", "russian_stemmer"],
},
rebuilt_Sorani: {
tokenizer: "standard",
filter: ["sorani_normalization", "lowercase", "decimal_digit", "sorani_stop", "sorani_keywords", "sorani_stemmer"],
},
rebuilt_Spanish: {
tokenizer: "standard",
filter: ["lowercase", "spanish_stop", "spanish_keywords", "spanish_stemmer"],
},
rebuilt_Swedish: {
tokenizer: "standard",
filter: ["lowercase", "swedish_stop", "swedish_keywords", "swedish_stemmer"],
},
rebuilt_Turkish: {
tokenizer: "standard",
filter: ["apostrophe", "turkish_lowercase", "turkish_stop", "turkish_keywords", "turkish_stemmer"],
},
rebuilt_Thai: {
tokenizer: "thai",
filter: ["lowercase", "decimal_digit", "thai_stop"],
}
}
}
};
================================================
FILE: source/templates/master/opensearch/info.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
ESInfo: {
Type: 'Custom::ESProxy',
Condition: 'DontCreateDomain',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
name: { Ref: 'OpenSearchName' },
},
},
ESInfoLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Condition: 'DontCreateDomain',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESInfoLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESInfoLambda: {
Type: 'AWS::Lambda::Function',
Condition: 'DontCreateDomain',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf-8'),
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'ESInfoLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'CustomResource',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
};
================================================
FILE: source/templates/master/opensearch/opensearch-dashboards/QnABotDashboard.json
================================================
{
"version": "1.3.0",
"objects": [
{
"id": "052b1350-a37d-11ea-8370-0f1df276cae1",
"type": "dashboard",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzAsMV0=",
"attributes": {
"hits": "0",
"timeFrom": "now/w",
"timeTo": "now/w",
"refreshInterval": {
"value": "0",
"pause": "true"
},
"description": "Visualize QnABot usage, see what your users are asking, and use the \"No Hits\" and \"Feedback\" charts to assess where you should add or tune QnABot content to make the bot smarter. ",
"title": "QnABot Dashboard",
"timeRestore": "false",
"version": "1",
"panelsJSON": "[{\"embeddableConfig\":{\"legendOpen\":false,\"vis\":{\"legendOpen\":true}},\"gridData\":{\"h\":15,\"i\":\"fb115451-3b8a-436f-b916-8a04db4e9d70\",\"w\":17,\"x\":0,\"y\":0},\"panelIndex\":\"fb115451-3b8a-436f-b916-8a04db4e9d70\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_0\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"5e25d094-b045-4afe-953d-2d619b05b716\",\"w\":14,\"x\":34,\"y\":0},\"panelIndex\":\"5e25d094-b045-4afe-953d-2d619b05b716\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_1\"},{\"embeddableConfig\":{\"legendOpen\":false,\"vis\":{\"legendOpen\":true}},\"gridData\":{\"h\":15,\"i\":\"cf017f39-a5a3-4d3a-9561-862f4c2eb3c5\",\"w\":17,\"x\":17,\"y\":0},\"panelIndex\":\"cf017f39-a5a3-4d3a-9561-862f4c2eb3c5\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_2\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"b9b730b1-b3de-42f9-a4de-69197d934a93\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"b9b730b1-b3de-42f9-a4de-69197d934a93\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_3\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"472ff8b6-83bf-4e4d-a8a5-44ce8f7e3dac\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"472ff8b6-83bf-4e4d-a8a5-44ce8f7e3dac\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_4\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"92e5cbb2-fa56-4f15-b7b1-72c11e0bebfc\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"92e5cbb2-fa56-4f15-b7b1-72c11e0bebfc\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_5\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"7ca7cdb0-2472-4eb0-bf7e-ae90f238f869\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"7ca7cdb0-2472-4eb0-bf7e-ae90f238f869\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_6\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":8,\"i\":\"cba70b74-3264-4153-87d2-68c24b552efa\",\"w\":10,\"x\":0,\"y\":45},\"panelIndex\":\"cba70b74-3264-4153-87d2-68c24b552efa\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_7\"},{\"embeddableConfig\":{},\"gridData\":{\"h\":15,\"i\":\"4fd7e920-26dd-4d02-8235-bcdff5725991\",\"w\":24,\"x\":10,\"y\":45},\"panelIndex\":\"4fd7e920-26dd-4d02-8235-bcdff5725991\",\"version\":\"7.9.1\",\"panelRefName\":\"panel_8\"}]",
"optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"
}
},
"references": [
{
"name": "panel_0",
"id": "a66d5ed0-a378-11ea-8370-0f1df276cae1",
"type": "visualization"
},
{
"name": "panel_1",
"id": "d905b930-a37a-11ea-a346-0f81312f0c3c",
"type": "visualization"
},
{
"name": "panel_2",
"id": "12d24870-e16c-11ea-b423-5f0e2ad2220e",
"type": "visualization"
},
{
"name": "panel_3",
"id": "68d7c450-a37a-11ea-8370-0f1df276cae1",
"type": "visualization"
},
{
"name": "panel_4",
"id": "d68ac390-a379-11ea-8370-0f1df276cae1",
"type": "visualization"
},
{
"name": "panel_5",
"id": "6759e170-a37b-11ea-8370-0f1df276cae1",
"type": "visualization"
},
{
"name": "panel_6",
"id": "985eb570-a37b-11ea-8370-0f1df276cae1",
"type": "visualization"
},
{
"name": "panel_7",
"id": "2031f610-a4c1-11ea-a012-c353d737e5ec",
"type": "visualization"
},
{
"name": "panel_8",
"id": "49e34620-9198-11eb-ab91-adc4ba11519d",
"type": "visualization"
}
],
"migrationVersion": {
"dashboard": "7.9.3"
}
},
{
"id": "a66d5ed0-a378-11ea-8370-0f1df276cae1",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzEsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Requests",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Requests\",\"type\":\"histogram\",\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"filter\":true,\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"category\"}],\"dimensions\":{\"x\":{\"accessor\":0,\"format\":{\"id\":\"date\",\"params\":{\"pattern\":\"HH:mm:ss\"}},\"params\":{\"date\":true,\"interval\":\"PT30S\",\"format\":\"HH:mm:ss\",\"bounds\":{\"min\":\"2020-07-06T21:55:15.220Z\",\"max\":\"2020-07-06T22:25:15.220Z\"}},\"aggType\":\"date_histogram\"},\"y\":[{\"accessor\":1,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"cardinality\"}]},\"grid\":{\"categoryLines\":false},\"labels\":{\"show\":false},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"thresholdLine\":{\"color\":\"#34130C\",\"show\":false,\"style\":\"full\",\"value\":10,\"width\":1},\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"datetime\",\"timeRange\":{\"from\":\"now-30m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"customLabel\":\"Requests\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"entireRequest.sentiment.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Sentiment\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "d905b930-a37a-11ea-a346-0f81312f0c3c",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzIsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Client Types",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Client Types\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":1,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"},\"buckets\":[{\"accessor\":0,\"format\":{\"id\":\"terms\",\"params\":{\"id\":\"string\",\"otherBucketLabel\":\"Other\",\"missingBucketLabel\":\"Missing\"}},\"params\":{},\"aggType\":\"terms\"}]}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"clientType.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Client Types\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "12d24870-e16c-11ea-b423-5f0e2ad2220e",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzMsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Requests AnswerSource",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"type\":\"histogram\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"datetime\",\"timeRange\":{\"from\":\"2020-08-18T15:44:48.334Z\",\"to\":\"2020-08-18T15:59:17.582Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"customLabel\":\"Requests\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"entireResponse.answerSource.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Source\"}}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"filter\":true,\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"category\"}],\"dimensions\":{\"x\":{\"accessor\":0,\"aggType\":\"date_histogram\",\"format\":{\"id\":\"date\",\"params\":{\"pattern\":\"HH:mm:ss\"}},\"params\":{\"bounds\":{\"max\":\"2020-07-06T22:25:15.220Z\",\"min\":\"2020-07-06T21:55:15.220Z\"},\"date\":true,\"format\":\"HH:mm:ss\",\"interval\":\"PT30S\"}},\"y\":[{\"accessor\":1,\"aggType\":\"cardinality\",\"format\":{\"id\":\"number\"},\"params\":{}}]},\"grid\":{\"categoryLines\":false},\"labels\":{\"show\":false},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"thresholdLine\":{\"color\":\"#34130C\",\"show\":false,\"style\":\"full\",\"value\":10,\"width\":1},\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Requests AnswerSource\"}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "68d7c450-a37a-11ea-8370-0f1df276cae1",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzQsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Logged Utterances",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Logged Utterances\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"metric\":{\"type\":\"vis_dimension\",\"accessor\":0,\"format\":{\"id\":\"string\",\"params\":{}}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"utterance.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":1000,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Logged Utterances\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "d68ac390-a379-11ea-8370-0f1df276cae1",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzUsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "No Hits",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"entireResponse.got_hits:0\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"No Hits\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"metric\":{\"type\":\"vis_dimension\",\"accessor\":0,\"format\":{\"id\":\"string\",\"params\":{}}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"utterance.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":1000,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"No Hits\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "6759e170-a37b-11ea-8370-0f1df276cae1",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzYsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Positive Feedback",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"feedback:correct\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Positive Feedback\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"metric\":{\"type\":\"vis_dimension\",\"accessor\":0,\"format\":{\"id\":\"string\",\"params\":{}}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"utterance.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Thumbs Up\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Feedback",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "985eb570-a37b-11ea-8370-0f1df276cae1",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzcsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "Negative Feedback",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"feedback:incorrect\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Negative Feedback\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"metric\":{\"type\":\"vis_dimension\",\"accessor\":1,\"format\":{\"id\":\"string\",\"params\":{}}},\"bucket\":{\"type\":\"vis_dimension\",\"accessor\":0,\"format\":{\"id\":\"terms\",\"params\":{\"id\":\"string\",\"otherBucketLabel\":\"Other\",\"missingBucketLabel\":\"Missing\"}}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"utterance.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":100,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Thumbs Down\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Feedback",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "2031f610-a4c1-11ea-a012-c353d737e5ec",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzgsMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{}",
"title": "QnAItemCount",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"QnAItemCount\",\"type\":\"metric\",\"params\":{\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"type\":\"range\",\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}},\"dimensions\":{\"metrics\":[{\"type\":\"vis_dimension\",\"accessor\":0,\"format\":{\"id\":\"number\",\"params\":{}}}]},\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"QnA Item Count\"}}]}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "QnaItems",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "49e34620-9198-11eb-ab91-adc4ba11519d",
"type": "visualization",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzksMV0=",
"attributes": {
"description": "",
"uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":0,\"direction\":\"asc\"}}}}",
"title": "Answer Sources",
"version": "1",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
},
"visState": "{\"title\":\"Answer Sources\",\"type\":\"table\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"entireResponse.result.answersource.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Answer Source\"},\"schema\":\"bucket\"}],\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"percentageCol\":\"\"}}"
},
"references": [
{
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
"id": "Metrics",
"type": "index-pattern"
}
],
"migrationVersion": {
"visualization": "7.10.0"
}
},
{
"id": "Metrics",
"type": "index-pattern",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzEwLDFd",
"attributes": {
"timeFieldName": "datetime",
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"answer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"answer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"answer\"}}},{\"name\":\"clientType\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"clientType.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"clientType\"}}},{\"name\":\"datetime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._clientType\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._clientType.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._clientType\"}}},{\"name\":\"entireRequest._event.bot.alias\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.bot.alias.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.bot.alias\"}}},{\"name\":\"entireRequest._event.bot.name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.bot.name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.bot.name\"}}},{\"name\":\"entireRequest._event.bot.version\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.bot.version.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.bot.version\"}}},{\"name\":\"entireRequest._event.currentIntent.confirmationStatus\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.currentIntent.confirmationStatus.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.currentIntent.confirmationStatus\"}}},{\"name\":\"entireRequest._event.currentIntent.name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.currentIntent.name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.currentIntent.name\"}}},{\"name\":\"entireRequest._event.currentIntent.slotDetails.slot.originalValue\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.currentIntent.slotDetails.slot.originalValue.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.currentIntent.slotDetails.slot.originalValue\"}}},{\"name\":\"entireRequest._event.currentIntent.slotDetails.slot.resolutions.value\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.currentIntent.slotDetails.slot.resolutions.value.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.currentIntent.slotDetails.slot.resolutions.value\"}}},{\"name\":\"entireRequest._event.currentIntent.slots.slot\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.currentIntent.slots.slot.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.currentIntent.slots.slot\"}}},{\"name\":\"entireRequest._event.errorFound\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._event.inputTranscript\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.inputTranscript.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.inputTranscript\"}}},{\"name\":\"entireRequest._event.invocationSource\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.invocationSource.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.invocationSource\"}}},{\"name\":\"entireRequest._event.messageVersion\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.messageVersion.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.messageVersion\"}}},{\"name\":\"entireRequest._event.outputDialogMode\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.outputDialogMode.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.outputDialogMode\"}}},{\"name\":\"entireRequest._event.recentIntentSummaryView.confirmationStatus\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.recentIntentSummaryView.confirmationStatus.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.recentIntentSummaryView.confirmationStatus\"}}},{\"name\":\"entireRequest._event.recentIntentSummaryView.dialogActionType\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.recentIntentSummaryView.dialogActionType.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.recentIntentSummaryView.dialogActionType\"}}},{\"name\":\"entireRequest._event.recentIntentSummaryView.fulfillmentState\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.recentIntentSummaryView.fulfillmentState.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.recentIntentSummaryView.fulfillmentState\"}}},{\"name\":\"entireRequest._event.recentIntentSummaryView.intentName\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.recentIntentSummaryView.intentName.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.recentIntentSummaryView.intentName\"}}},{\"name\":\"entireRequest._event.recentIntentSummaryView.slots.slot\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.recentIntentSummaryView.slots.slot.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.recentIntentSummaryView.slots.slot\"}}},{\"name\":\"entireRequest._event.sessionAttributes.qnabot_gotanswer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.sessionAttributes.qnabot_gotanswer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.sessionAttributes.qnabot_gotanswer\"}}},{\"name\":\"entireRequest._event.sessionAttributes.qnabot_qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.sessionAttributes.qnabot_qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.sessionAttributes.qnabot_qid\"}}},{\"name\":\"entireRequest._event.sessionAttributes.qnabotcontext\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.sessionAttributes.qnabotcontext.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.sessionAttributes.qnabotcontext\"}}},{\"name\":\"entireRequest._event.userId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._event.userId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._event.userId\"}}},{\"name\":\"entireRequest._info.es.address\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._info.es.address.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._info.es.address\"}}},{\"name\":\"entireRequest._info.es.index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._info.es.index.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._info.es.index\"}}},{\"name\":\"entireRequest._info.es.service.proxy\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._info.es.service.proxy.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._info.es.service.proxy\"}}},{\"name\":\"entireRequest._info.es.service.qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._info.es.service.qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._info.es.service.qid\"}}},{\"name\":\"entireRequest._info.es.type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._info.es.type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._info.es.type\"}}},{\"name\":\"entireRequest._preferredResponseType\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._preferredResponseType.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._preferredResponseType\"}}},{\"name\":\"entireRequest._settings.ALT_SEARCH_KENDRA_INDEXES\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ALT_SEARCH_KENDRA_INDEXES.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ALT_SEARCH_KENDRA_INDEXES\"}}},{\"name\":\"entireRequest._settings.DEFAULT_ALEXA_LAUNCH_MESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.DEFAULT_ALEXA_LAUNCH_MESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.DEFAULT_ALEXA_LAUNCH_MESSAGE\"}}},{\"name\":\"entireRequest._settings.DEFAULT_ALEXA_STOP_MESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.DEFAULT_ALEXA_STOP_MESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.DEFAULT_ALEXA_STOP_MESSAGE\"}}},{\"name\":\"entireRequest._settings.DEFAULT_USER_POOL_JWKS_URL\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.DEFAULT_USER_POOL_JWKS_URL.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.DEFAULT_USER_POOL_JWKS_URL\"}}},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_BOT_FAILURE_MESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_BOT_FAILURE_MESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ELICIT_RESPONSE_BOT_FAILURE_MESSAGE\"}}},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_DEFAULT_MSG\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_DEFAULT_MSG.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ELICIT_RESPONSE_DEFAULT_MSG\"}}},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_MAX_RETRIES\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_RETRY_MESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ELICIT_RESPONSE_RETRY_MESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ELICIT_RESPONSE_RETRY_MESSAGE\"}}},{\"name\":\"entireRequest._settings.EMPTYMESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.EMPTYMESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.EMPTYMESSAGE\"}}},{\"name\":\"entireRequest._settings.ENABLE_DEBUG_RESPONSES\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ENABLE_MULTI_LANGUAGE_SUPPORT\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ENABLE_REDACTING\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ENABLE_SENTIMENT_SUPPORT\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ENFORCE_VERIFIED_IDENTITY\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ERRORMESSAGE\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ERRORMESSAGE.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ERRORMESSAGE\"}}},{\"name\":\"entireRequest._settings.ES_KEYWORD_SYNTAX_TYPES\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ES_KEYWORD_SYNTAX_TYPES.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ES_KEYWORD_SYNTAX_TYPES\"}}},{\"name\":\"entireRequest._settings.ES_MINIMUM_SHOULD_MATCH\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ES_MINIMUM_SHOULD_MATCH.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ES_MINIMUM_SHOULD_MATCH\"}}},{\"name\":\"entireRequest._settings.ES_NO_HITS_QUESTION\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ES_NO_HITS_QUESTION.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ES_NO_HITS_QUESTION\"}}},{\"name\":\"entireRequest._settings.ES_PHRASE_BOOST\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ES_PHRASE_BOOST.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ES_PHRASE_BOOST\"}}},{\"name\":\"entireRequest._settings.ES_SCORE_ANSWER_FIELD\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ES_SYNTAX_CONFIDENCE_LIMIT\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.ES_SYNTAX_CONFIDENCE_LIMIT.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.ES_SYNTAX_CONFIDENCE_LIMIT\"}}},{\"name\":\"entireRequest._settings.ES_USE_FUZZY_MATCH\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.ES_USE_KEYWORD_FILTERS\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.KENDRA_FAQ_CONFIG_MAX_RETRIES\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.KENDRA_FAQ_CONFIG_RETRY_DELAY\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.KENDRA_FAQ_ES_FALLBACK\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.KENDRA_FAQ_INDEX\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.KENDRA_FAQ_INDEX.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.KENDRA_FAQ_INDEX\"}}},{\"name\":\"entireRequest._settings.MINIMUM_CONFIDENCE_SCORE\",\"type\":\"number\",\"esTypes\":[\"float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.NO_VERIFIED_IDENTITY_QUESTION\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.NO_VERIFIED_IDENTITY_QUESTION.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.NO_VERIFIED_IDENTITY_QUESTION\"}}},{\"name\":\"entireRequest._settings.REDACTING_REGEX\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.REDACTING_REGEX.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.REDACTING_REGEX\"}}},{\"name\":\"entireRequest._settings.SMS_HINT_REMINDER\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.SMS_HINT_REMINDER.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.SMS_HINT_REMINDER\"}}},{\"name\":\"entireRequest._settings.SMS_HINT_REMINDER_ENABLE\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._settings.SMS_HINT_REMINDER_INTERVAL_HRS\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._settings.SMS_HINT_REMINDER_INTERVAL_HRS.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._settings.SMS_HINT_REMINDER_INTERVAL_HRS\"}}},{\"name\":\"entireRequest._type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._type\"}}},{\"name\":\"entireRequest._userId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._userId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._userId\"}}},{\"name\":\"entireRequest._userInfo.FirstSeen\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._userInfo.FirstSeen.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._userInfo.FirstSeen\"}}},{\"name\":\"entireRequest._userInfo.InteractionCount\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._userInfo.LastSeen\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._userInfo.LastSeen.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._userInfo.LastSeen\"}}},{\"name\":\"entireRequest._userInfo.TimeSinceLastInteraction\",\"type\":\"number\",\"esTypes\":[\"float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest._userInfo.UserId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._userInfo.UserId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._userInfo.UserId\"}}},{\"name\":\"entireRequest._userInfo.isVerifiedIdentity\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest._userInfo.isVerifiedIdentity.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest._userInfo.isVerifiedIdentity\"}}},{\"name\":\"entireRequest.kendraResultsCached\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.kendraResultsCached.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.kendraResultsCached\"}}},{\"name\":\"entireRequest.question\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.question.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.question\"}}},{\"name\":\"entireRequest.sentiment\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.sentiment.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.sentiment\"}}},{\"name\":\"entireRequest.session.qnabot_gotanswer\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest.session.qnabot_qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabot_qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabot_qid\"}}},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraIndexId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraIndexId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.kendra.kendraIndexId\"}}},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraQueryId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraQueryId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.kendra.kendraQueryId\"}}},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraResponsibleQid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraResponsibleQid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.kendra.kendraResponsibleQid\"}}},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraResultId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.kendra.kendraResultId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.kendra.kendraResultId\"}}},{\"name\":\"entireRequest.session.qnabotcontext.navigation.hasParent\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireRequest.session.qnabotcontext.navigation.next\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.navigation.next.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.navigation.next\"}}},{\"name\":\"entireRequest.session.qnabotcontext.previous.a\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.previous.a.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.previous.a\"}}},{\"name\":\"entireRequest.session.qnabotcontext.previous.q\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.previous.q.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.previous.q\"}}},{\"name\":\"entireRequest.session.qnabotcontext.previous.qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireRequest.session.qnabotcontext.previous.qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireRequest.session.qnabotcontext.previous.qid\"}}},{\"name\":\"entireResponse._userInfo.FirstSeen\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse._userInfo.FirstSeen.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse._userInfo.FirstSeen\"}}},{\"name\":\"entireResponse._userInfo.InteractionCount\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse._userInfo.LastSeen\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse._userInfo.LastSeen.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse._userInfo.LastSeen\"}}},{\"name\":\"entireResponse._userInfo.TimeSinceLastInteraction\",\"type\":\"number\",\"esTypes\":[\"float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse._userInfo.UserId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse._userInfo.UserId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse._userInfo.UserId\"}}},{\"name\":\"entireResponse._userInfo.isVerifiedIdentity\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse._userInfo.isVerifiedIdentity.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse._userInfo.isVerifiedIdentity\"}}},{\"name\":\"entireResponse.answerSource\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.answerSource.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.answerSource\"}}},{\"name\":\"entireResponse.card.send\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.card.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.card.text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.card.text\"}}},{\"name\":\"entireResponse.card.title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.card.title.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.card.title\"}}},{\"name\":\"entireResponse.card.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.card.url.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.card.url\"}}},{\"name\":\"entireResponse.got_hits\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.QueryId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.QueryId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.QueryId\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Key\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Key.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Key\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Highlights.BeginOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Highlights.EndOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Highlights.TopAnswer\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.Value.TextWithHighlightsValue.Text\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.ValueType\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.ValueType.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.AdditionalAttributes.ValueType\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Key\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Key.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Key\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Value.StringValue\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Value.StringValue.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentAttributes.Value.StringValue\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Highlights.BeginOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Highlights.EndOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Highlights.TopAnswer\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentExcerpt.Text\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentId\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Highlights.BeginOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Highlights.EndOffset\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Highlights.TopAnswer\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentTitle.Text\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentURI\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.DocumentURI.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.DocumentURI\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.Id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.Id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.Id\"}}},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.Type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.ResultItems.Type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.ResultItems.Type\"}}},{\"name\":\"entireResponse.kendraResultsCached.TotalNumberOfResults\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.kendraResultsCached.originalKendraIndexId\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.kendraResultsCached.originalKendraIndexId.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.kendraResultsCached.originalKendraIndexId\"}}},{\"name\":\"entireResponse.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.message\"}}},{\"name\":\"entireResponse.plainMessage\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.plainMessage.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.plainMessage\"}}},{\"name\":\"entireResponse.result.a\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.a.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.a\"}}},{\"name\":\"entireResponse.result.answersource\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.answersource.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.answersource\"}}},{\"name\":\"entireResponse.result.autotranslate.a\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"entireResponse.result.l\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.l.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.l\"}}},{\"name\":\"entireResponse.result.q\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.q.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.q\"}}},{\"name\":\"entireResponse.result.qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.qid\"}}},{\"name\":\"entireResponse.result.questions.q\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.questions.q.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.questions.q\"}}},{\"name\":\"entireResponse.result.quniqueterms\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.quniqueterms.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.quniqueterms\"}}},{\"name\":\"entireResponse.result.type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.result.type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.result.type\"}}},{\"name\":\"entireResponse.session.appContext\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.session.appContext.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.session.appContext\"}}},{\"name\":\"entireResponse.session.qnabot_gotanswer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.session.qnabot_gotanswer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.session.qnabot_gotanswer\"}}},{\"name\":\"entireResponse.session.qnabot_qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.session.qnabot_qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.session.qnabot_qid\"}}},{\"name\":\"entireResponse.session.qnabotcontext\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.session.qnabotcontext.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.session.qnabotcontext\"}}},{\"name\":\"entireResponse.type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"entireResponse.type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"entireResponse.type\"}}},{\"name\":\"qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"qid\"}}},{\"name\":\"topic\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"topic.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"topic\"}}},{\"name\":\"utterance\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"utterance.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"utterance\"}}}]",
"title": ""
},
"references": [],
"migrationVersion": {
"index-pattern": "7.6.0"
}
},
{
"id": "Feedback",
"type": "index-pattern",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzExLDFd",
"attributes": {
"timeFieldName": "datetime",
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"alternate\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"alternate.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"alternate\"}}},{\"name\":\"answer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"answer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"answer\"}}},{\"name\":\"datetime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"feedback\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"feedback.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"feedback\"}}},{\"name\":\"qid\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"qid.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"qid\"}}},{\"name\":\"utterance\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"utterance.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"utterance\"}}}]",
"title": ""
},
"references": [],
"migrationVersion": {
"index-pattern": "7.6.0"
}
},
{
"id": "QnaItems",
"type": "index-pattern",
"namespaces": [
"default"
],
"updated_at": "2022-12-04T21:21:27.535Z",
"version": "WzEyLDFd",
"attributes": {
"fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"a\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"alt.markdown\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"alt.markdown.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"alt.markdown\"}}},{\"name\":\"alt.ssml\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"alt.ssml.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"alt.ssml\"}}},{\"name\":\"args\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"args.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"args\"}}},{\"name\":\"conditionalChaining\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"conditionalChaining.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"conditionalChaining\"}}},{\"name\":\"correctAnswers\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"elicitResponse.response_sessionattr_namespace\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"elicitResponse.response_sessionattr_namespace.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"elicitResponse.response_sessionattr_namespace\"}}},{\"name\":\"elicitResponse.responsebot_hook\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"elicitResponse.responsebot_hook.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"elicitResponse.responsebot_hook\"}}},{\"name\":\"incorrectAnswers\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"l\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"next\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"next.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"next\"}}},{\"name\":\"qid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"question\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"questions.q\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"quiz\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"quiz.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"quiz\"}}},{\"name\":\"quniqueterms\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.buttons.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.buttons.text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"r.buttons.text\"}}},{\"name\":\"r.buttons.value\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.buttons.value.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"r.buttons.value\"}}},{\"name\":\"r.imageUrl\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"r.subTitle\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.subTitle.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"r.subTitle\"}}},{\"name\":\"r.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.text.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"r.text\"}}},{\"name\":\"r.title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"r.url.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"r.url\"}}},{\"name\":\"responses.correct\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"responses.correct.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"responses.correct\"}}},{\"name\":\"responses.end\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"responses.end.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"responses.end\"}}},{\"name\":\"responses.incorrect\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"responses.incorrect.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"responses.incorrect\"}}},{\"name\":\"selected\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"t\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"type.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"type\"}}}]",
"title": ""
},
"references": [],
"migrationVersion": {
"index-pattern": "7.6.0"
}
}
]
}
================================================
FILE: source/templates/master/opensearch/opensearch-dashboards/README.md
================================================
# OpenSearch Dashboards
JSON exported from Opensearch Dashboards using
```bash
curl -X GET 'master-user:master-user-password' "https:///_dashboards/api/opensearch-dashboards/dashboards/export?dashboard=052b1350-a37d-11ea-8370-0f1df276cae1" > QnABotDashboard.json
```
After exporting, edit the 3 index-pattern sections to replace actual index 'title' fields with tokens: , , - 1 occurrence each, e.g.:
```json
{
"id": "QnaItems",
"type": "index-pattern",
"updated_at": "2020-06-02T10:44:21.050Z",
"version": "WzEzLDFd",
"attributes": {
"title": "",
"fields": ...
```
================================================
FILE: source/templates/master/opensearch/proxy.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
ESCFNProxyLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESCFNProxyLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESCFNProxyLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Environment: {
Variables: {
SETTINGS_TABLE: { Ref: 'SettingsTable' },
...util.getCommonEnvironmentVariables(),
},
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'CfnLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' }],
Handler: 'resource.handler',
LoggingConfig: {
LogGroup: { Ref: 'ESCFNProxyLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'CustomResource',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
MetricsIndex: {
Type: 'Custom::ESProxy',
DependsOn: ['OpensearchDomain'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['ESCFNProxyLambda', 'Arn'] },
create: {
index: { 'Fn::Sub': '${Var.MetricsIndex}' },
endpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
body: {
'Fn::Sub': JSON.stringify({
settings: { 'index.mapping.total_fields.limit': 2000 },
}),
},
},
},
},
FeedbackIndex: {
Type: 'Custom::ESProxy',
DependsOn: ['OpensearchDomain'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['ESCFNProxyLambda', 'Arn'] },
create: {
index: { 'Fn::Sub': '${Var.FeedbackIndex}' },
endpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
body: {
'Fn::Sub': JSON.stringify({
settings: {},
}),
},
},
},
},
Index: {
Type: 'Custom::ESProxy',
DependsOn: ['OpensearchDomain'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['ESCFNProxyLambda', 'Arn'] },
create: {
index: { 'Fn::Sub': '${Var.QnaIndex}' },
endpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
body: {
'Fn::Sub': [
JSON.stringify({
settings: require('./index_settings.js'),
mappings: require('./index_mappings.js'),
}),
{
EmbeddingsDimensions: {
'Fn::If': [
'EmbeddingsEnable',
{
'Fn::If': [
'EmbeddingsLambda',
{ Ref: 'EmbeddingsLambdaDimensions' },
{
'Fn::If': [
'EmbeddingsBedrock',
{ 'Fn::FindInMap': ['BedrockDefaults', { Ref : 'EmbeddingsBedrockModelId' }, 'EmbeddingsDimensions'] },
'INVALID EMBEDDINGS API - Cannot determine dimensions',
],
},
],
},
'1', // minimal default to use if embeddings are disabled
],
},
},
],
},
},
},
},
OpensearchDashboards: {
Type: 'Custom::ESProxy',
DependsOn: ['Index'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['ESCFNProxyLambda', 'Arn'] },
create: {
endpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
path: '/_dashboards/api/opensearch-dashboards/dashboards/import?force=true',
method: 'POST',
headers: { 'osd-xsrf': 'true' },
body: require('./opensearch-dashboards/QnABotDashboard'),
replaceTokenInBody: [
{ f: '', r: { 'Fn::Sub': '${Var.QnaIndex}' } },
{ f: '', r: { 'Fn::Sub': '${Var.MetricsIndex}' } },
{ f: '', r: { 'Fn::Sub': '${Var.FeedbackIndex}' } },
],
},
},
},
};
================================================
FILE: source/templates/master/opensearch/updates.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
OpenSearchLogGroup: {
Type: 'AWS::Logs::LogGroup',
Condition: 'FGACEnabled',
Properties: {
LogGroupName: { 'Fn::Sub': '/aws/opensearch/${AWS::StackName}-${ESVar.ESDomain}' },
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' }
]
},
},
Metadata: {
cfn_nag: {
rules_to_suppress: [
{
id: 'W86',
reason: 'LogGroup is encrypted by default.',
}
],
},
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
OpenSearchLogGroupResourcePolicy: {
Type: 'AWS::Logs::ResourcePolicy',
Condition: 'FGACEnabled',
DependsOn: ['OpenSearchLogGroup'],
Properties: {
PolicyName: { 'Fn::Sub': '${AWS::StackName}-AWSQnaBotOpenSearchLogResourcePolicy' },
PolicyDocument: JSON.stringify(util.openSearchLogResourcePolicy())
}
},
OpenSearchCognitoAccessUpdates: {
DependsOn: [
'OpensearchDomain',
'Index',
'FeedbackIndex',
'MetricsIndex',
'ESCognitoRole',
'OpenSearchLogGroupResourcePolicy'
],
Type: 'Custom::OpenSearchUpdates',
Condition: 'FGACEnabled',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
DomainName: { 'Fn::GetAtt': ['ESVar', 'ESDomain'] },
AccessPolicies: util.openSearchAccessPolicy(),
LogPublishingOptions: {
SEARCH_SLOW_LOGS: {
CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': ['OpenSearchLogGroup', 'Arn'] },
Enabled: true
},
INDEX_SLOW_LOGS: {
CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': ['OpenSearchLogGroup', 'Arn'] },
Enabled: true
},
AUDIT_LOGS: {
CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': ['OpenSearchLogGroup', 'Arn'] },
Enabled: true
},
ES_APPLICATION_LOGS: {
CloudWatchLogsLogGroupArn: { 'Fn::GetAtt': ['OpenSearchLogGroup', 'Arn'] },
Enabled: true
}
},
AdvancedSecurityOptions: util.advancedSecurityOptions(),
}
}
};
================================================
FILE: source/templates/master/policies.json
================================================
{
"LexAccessPolicy":{
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"lex:RecognizeText",
"lex:RecognizeUtterance"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:*"
}
]
},{
"Effect": "Allow",
"Action": [
"polly:SynthesizeSpeech"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:polly:${AWS::Region}:${AWS::AccountId}:*"
}
]
}]
},
"Roles":{"Fn::If":[
"Public",
[{"Ref":"AdminRole"},{"Ref":"UnauthenticatedRole"},{"Ref":"UserRole"}],
[{"Ref":"AdminRole"},{"Ref":"UserRole"}]
]}
},
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [{
"id": "W13",
"reason": "This policy is required to have * resource"
}]
}
}
},
"ApiGatewayCloudWatchLogsRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Policies": [
{
"PolicyName": "ApiGatewayLogsPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"logs:GetLogEvents",
"logs:FilterLogEvents"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*"
}
]
}
]
}
}
]
},
"Metadata":{
"cfn_nag": {
"rules_to_suppress": [{
"id": "W11",
"reason": "This IAM role requires to have * resource on its permission policy"
}]
},
"guard": {
"SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK"]
}
}
},
"ApiGatewayRole": {
"Type": "AWS::IAM::Role",
"Metadata": {
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK"] }
},
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
}
}
}
}
================================================
FILE: source/templates/master/proxy-es.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const util = require('../util');
const examples = _.fromPairs(require('../examples/outputs')
.names
.map((x) => [x, { 'Fn::GetAtt': ['ExamplesStack', `Outputs.${x}`] }]));
module.exports = {
ESProxyCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
BuildDate: (new Date()).toISOString(),
},
},
UtteranceLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-UtteranceLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
UtteranceLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' }],
Environment: {
Variables: {
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::Join': ['', ['https://', { 'Fn::GetAtt': ['ESVar', 'ESAddress'] }]] },
UTTERANCE_BUCKET: { Ref: 'AssetBucket' },
UTTERANCE_KEY: 'default-utterances.json',
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.utterances',
LoggingConfig: {
LogGroup: { Ref: 'UtteranceLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Service',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ESQidLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESQidLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESQidLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' }],
Environment: {
Variables: {
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.qid',
LoggingConfig: {
LogGroup: { Ref: 'ESQidLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Service',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ESCleaningLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESCleaningLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESCleaningLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' }],
Environment: {
Variables: {
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
FEEDBACK_DELETE_RANGE_MINUTES: { Ref: 'OpenSearchDashboardsRetentionMinutes' },
METRICS_DELETE_RANGE_MINUTES: { Ref: 'OpenSearchDashboardsRetentionMinutes' },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.cleanmetrics',
LoggingConfig: {
LogGroup: { Ref: 'ESCleaningLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Service',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ScheduledESCleaning: {
Type: 'AWS::Events::Rule',
Properties: {
Description: '',
ScheduleExpression: 'rate(1 day)',
State: 'ENABLED',
Targets: [{
Arn: { 'Fn::GetAtt': ['ESCleaningLambda', 'Arn'] },
Id: 'ES_Cleaning_Function',
}],
},
},
PermissionForEventsToInvokeLambda: {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: { Ref: 'ESCleaningLambda' },
Action: 'lambda:InvokeFunction',
Principal: 'events.amazonaws.com',
SourceArn: { 'Fn::GetAtt': ['ScheduledESCleaning', 'Arn'] },
},
},
ESLoggingLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESLoggingLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESLoggingLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' },
],
Environment: {
Variables: {
FIREHOSE_NAME: { Ref: 'GeneralKinesisFirehose' },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.logging',
LoggingConfig: {
LogGroup: { Ref: 'ESLoggingLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESLoggingLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Logging',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ESQueryLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESQueryLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESQueryLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Environment: {
Variables: {
'Fn::If': [
'BuildExamples',
{
SETTINGS_TABLE: { Ref: 'SettingsTable' },
DEFAULT_SETTINGS_PARAM: { Ref: 'DefaultQnABotSettings' },
...examples,
...util.getCommonEnvironmentVariables(),
},
{
DEFAULT_SETTINGS_PARAM: { Ref: 'DefaultQnABotSettings' },
...util.getCommonEnvironmentVariables(),
},
],
},
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' }],
Handler: 'index.query',
LoggingConfig: {
LogGroup: { Ref: 'ESQueryLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Query',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ESProxyLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ESProxyLambdaLogGroup' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ESProxyLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/proxy-es.zip' },
S3ObjectVersion: { Ref: 'ESProxyCodeVersion' },
},
Layers: [{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
{ Ref: 'EsProxyLambdaLayer' },
{ Ref: 'QnABotCommonLambdaLayer' },
],
Environment: {
Variables: {
ES_TYPE: { 'Fn::GetAtt': ['Var', 'QnAType'] },
ES_INDEX: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
ES_ADDRESS: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
SETTINGS_TABLE: { Ref: 'SettingsTable' },
EMBEDDINGS_API: { Ref: 'EmbeddingsApi' },
EMBEDDINGS_LAMBDA_ARN: { Ref: 'EmbeddingsLambdaArn' },
DEFAULT_SETTINGS_PARAM: { Ref: 'DefaultQnABotSettings' },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'ESProxyLambdaLogGroup' },
},
MemorySize: '1408',
Role: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Tags: [{
Key: 'Type',
Value: 'Service',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ESProxyEmbeddingsPolicyResources: {
Type: 'Custom::ModelAccess',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
EmbeddingsBedrockModelId: { 'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'ModelID'] }, { Ref: 'AWS::NoValue' }] },
},
},
ESProxyLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
ManagedPolicyArns: [
{ Ref: 'QueryPolicy' },
],
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
util.translateReadOnly(),
util.lexFullAccess(),
{
PolicyName: 'ParamStorePolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['ssm:GetParameter', 'ssm:GetParameters'],
Resource: [
{
'Fn::Join': [
'', [
'arn:',
{ 'Fn::Sub': '${AWS::Partition}:' },
'ssm:',
{ 'Fn::Sub': '${AWS::Region}:' },
{ 'Fn::Sub': '${AWS::AccountId}:' },
'parameter/',
{ Ref: 'DefaultUserPoolJwksUrl' },
],
],
},
],
}],
},
},
{
'Fn::If': [
'EmbeddingsEnable',
{
PolicyName: 'EmbeddingsPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
'Fn::If': [
'EmbeddingsLambdaArn',
{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [{ Ref: 'EmbeddingsLambdaArn' }],
},
{ Ref: 'AWS::NoValue' },
],
},
{
'Fn::If': [
'EmbeddingsBedrock',
{
Effect: 'Allow',
Action: [
'bedrock:InvokeModel',
],
Resource: { 'Fn::GetAtt': ['ESProxyEmbeddingsPolicyResources', 'modelArn'] },
},
{ Ref: 'AWS::NoValue' },
],
},
],
},
},
{ Ref: 'AWS::NoValue' },
],
},
{
PolicyName: 'S3QNABucketReadAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
's3:GetObject',
's3:ListBucket',
],
Resource: [
'arn:aws:s3:::QNA*/*',
'arn:aws:s3:::qna*/*',
],
},
],
},
},
{
PolicyName: 'SettingsTableReadAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'dynamodb:Scan',
],
Resource: [{ 'Fn::GetAtt': ['SettingsTable', 'Arn'] }],
},
],
},
}
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12', 'W76', 'F3']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
QueryLambdaInvokePolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
'Fn::If': [
'BuildExamples',
{
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['lambda:InvokeFunction'],
Resource: [
'arn:aws:lambda:*:*:function:qna*',
'arn:aws:lambda:*:*:function:QNA*',
].concat(require('../examples/outputs').names
.map((x) => ({ 'Fn::GetAtt': ['ExamplesStack', `Outputs.${x}`] }))),
}],
},
{
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['lambda:InvokeFunction'],
Resource: [
'arn:aws:lambda:*:*:function:qna*',
'arn:aws:lambda:*:*:function:QNA*',
],
}],
},
],
},
Roles: [{ Ref: 'ESProxyLambdaRole' }],
},
},
ESLoggingLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'LambdaGeneralKinesisFirehoseQNALambda',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'lambda:InvokeFunction',
],
Resource: [
{ 'Fn::Join': ['', ['arn:aws:lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:qna-*']] },
{ 'Fn::Join': ['', ['arn:aws:lambda:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':function:QNA-*']] },
],
},
{
Effect: 'Allow',
Action: [
'comprehend:DetectPiiEntities',
],
Resource: [
'*',
],
},
{
Effect: 'Allow',
Action: [
'firehose:PutRecord',
'firehose:PutRecordBatch',
],
Resource: [
{ 'Fn::GetAtt': ['GeneralKinesisFirehose', 'Arn'] },
],
},
],
},
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
QueryPolicy: {
Type: 'AWS::IAM::ManagedPolicy',
Properties: {
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'es:ESHttp*',
],
Resource: [
{ 'Fn::Sub': '${ESVar.ESArn}/*' },
],
}, {
Effect: 'Allow',
Action: [
'kendra:Query',
'kendra:Retrieve',
],
Resource: [
{ 'Fn::Sub': 'arn:aws:kendra:${AWS::Region}:${AWS::AccountId}:index/*' },
],
}, {
Effect: 'Allow',
Action: ['s3:Get*'],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${AssetBucket}*' },
],
},
{
Effect: 'Allow',
Action: ['comprehend:DetectSyntax'],
Resource: ['*'], // these actions cannot be bound to resources other than *
},
],
},
},
Metadata: { cfn_nag: util.cfnNag(['F5', 'W13']) },
},
};
================================================
FILE: source/templates/master/proxy-lex/README.md
================================================
# Lex Proxy Lambda
lambda to proxy apigateway request to lex
================================================
FILE: source/templates/master/proxy-lex/handler.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { LexModelBuildingService } = require('@aws-sdk/client-lex-model-building-service');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const lex = new LexModelBuildingService(customSdkConfig('C001', { region }));
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
const result = await lex[event.fnc](event.params);
console.log(`Response: ${JSON.stringify(result, null, 2)}`);
return result;
} catch (error) {
console.log(`Error: ${error}`);
throw JSON.stringify({
type: '[InternalServiceError]',
data: error,
});
}
};
================================================
FILE: source/templates/master/proxy-lex/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
LexProxyLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexProxyLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
LexProxyLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf8'),
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'LexProxyLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['LexProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
LexStatusLambdaLogGroup:{
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-LexStatusLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
LexStatusLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/status.js`, 'utf8'),
},
Environment: {
Variables: {
STATUS_BUCKET: { Ref: 'BuildStatusBucket' },
LEXV2_STATUS_KEY: 'lexV2status.json',
FULFILLMENT_FUNCTION_ARN: {
'Fn::Join': [':', [
{ 'Fn::GetAtt': ['FulfillmentLambda', 'Arn'] },
'live',
]],
},
FULFILLMENT_FUNCTION_ROLE: { Ref: 'FulfillmentLambdaRole' },
LEXV2_BOT_NAME: { 'Fn::GetAtt': ['LexV2Bot', 'botName'] },
LEXV2_BOT_ID: { 'Fn::GetAtt': ['LexV2Bot', 'botId'] },
LEXV2_BOT_ALIAS: { 'Fn::GetAtt': ['LexV2Bot', 'botAlias'] },
LEXV2_BOT_ALIAS_ID: { 'Fn::GetAtt': ['LexV2Bot', 'botAliasId'] },
LEXV2_INTENT: { 'Fn::GetAtt': ['LexV2Bot', 'botIntent'] },
LEXV2_INTENT_FALLBACK: { 'Fn::GetAtt': ['LexV2Bot', 'botIntentFallback'] },
LEXV2_BOT_LOCALE_IDS: { 'Fn::GetAtt': ['LexV2Bot', 'botLocaleIds'] },
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'LexStatusLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['LexProxyLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
LexProxyLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.lexFullAccess(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'Access',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
's3:Get*',
],
Resource: [{ 'Fn::Sub': 'arn:aws:s3:::${BuildStatusBucket}*' }],
}],
},
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12', 'W76', 'F3']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
================================================
FILE: source/templates/master/proxy-lex/status.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { LexModelsV2Client, DescribeBotCommand } = require('@aws-sdk/client-lex-models-v2');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const s3 = new S3Client(customSdkConfig('C022', { region }));
const lexv2 = new LexModelsV2Client(customSdkConfig('C002', { region }));
function getStatusResponse(response, build) {
const botStatus = (response.botStatus == 'Available') ? 'READY' : response.botStatus;
const statusResponse = {
lambdaArn: process.env.FULFILLMENT_FUNCTION_ARN,
lambdaRole: process.env.FULFILLMENT_FUNCTION_ROLE,
botversion: 'live',
lexV2botname: process.env.LEXV2_BOT_NAME || 'LEX V2 Bot not installed',
lexV2botid: process.env.LEXV2_BOT_ID || 'LEX V2 Bot not installed',
lexV2botalias: process.env.LEXV2_BOT_ALIAS || 'LEX V2 Bot not installed',
lexV2botaliasid: process.env.LEXV2_BOT_ALIAS_ID || 'LEX V2 Bot not installed',
lexV2intent: process.env.LEXV2_INTENT || 'LEX V2 Bot not installed',
lexV2intentFallback: process.env.LEXV2_INTENT_FALLBACK || 'LEX V2 Bot not installed',
lexV2localeids: process.env.LEXV2_BOT_LOCALE_IDS || 'LEX V2 Bot not installed',
status: botStatus,
build,
};
return statusResponse;
}
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
const bucket = process.env.STATUS_BUCKET;
const lexV2StatusFile = process.env.LEXV2_STATUS_KEY;
let build = { status: 'READY', token: 'token' };
let response;
try {
const getObjCmd = new GetObjectCommand({ Bucket: bucket, Key: lexV2StatusFile });
response = await s3.send(getObjCmd);
const readableStreamV2 = Buffer.concat(await response.Body.toArray());
build = JSON.parse(readableStreamV2);
} catch (e) {
console.log('Unable to read S3 lex bot status file - perhaps it doesn\'t yet exist. Returning READY');
}
const describeBotCmd = new DescribeBotCommand({
botId: process.env.LEXV2_BOT_ID,
});
response = await lexv2.send(describeBotCmd);
const statusResponse = getStatusResponse(response, build);
return statusResponse;
};
================================================
FILE: source/templates/master/proxy-lex/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.env.AWS_PROFILE = require('../../../config.json').profile;
process.env.AWS_DEFAULT_REGION = require('../../../config.json').region;
process.env.AWS_REGION = require('../../../config.json').region;
const { handler } = require('./handler');
module.exports = {
get(test) {
handler({
fnc: 'getBots',
params: { maxResults: 2 },
}, {}, (err, result) => {
console.log('error', err);
console.log('result:', JSON.stringify(result, null, 2));
test.ifError(err);
test.ok(result);
test.done();
});
},
};
================================================
FILE: source/templates/master/roles.json
================================================
{
"OpenSearchDashboardsRole": {
"Type": "AWS::IAM::Role",
"Metadata": {
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK", "CFN_NO_EXPLICIT_RESOURCE_NAMES"] }
},
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "OpenSearchDashboardsIdPool"
}
}
}
}
]
},
"RoleName": {
"Fn::Join": [
"",
[
{
"Fn::Select": ["0",
{
"Fn::Split": ["-",
{
"Fn::Select" : [2,
{
"Fn::Split": ["/",
{
"Ref": "AWS::StackId"
}
]
}
]
}]
}]
},
"-OpenSearchDashboardsRole"
]
]},
"Path": "/",
"Policies": [
{
"PolicyName": "OpenSearchDashboardsAccessPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CognitoAuth",
"Effect": "Allow",
"Action": "es:ESHttp*",
"Resource": {
"Fn::Sub": "${ESVar.ESArn}/*"
}
}
]
}
}
]
}
},
"AdminRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "IdPool"
}
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
},
"Path": "/",
"RoleName": {
"Fn::Join": [
"",
[
{
"Fn::Select": ["0",
{
"Fn::Split": ["-",
{
"Fn::Select" : [2,
{
"Fn::Split": ["/",
{
"Ref": "AWS::StackId"
}
]
}
]
}]
}]
},
"-AdminRole"
]
]},
"Policies": [
{
"PolicyName": "apiAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"es:ESHttp*"
],
"Resource": [
{
"Fn::GetAtt": [
"ESVar",
"ESArn"
]
}
]
},
{
"Effect": "Allow",
"Action": [
"cognito-idp:AdminUserGlobalSignOut"
],
"Resource": [
{
"Fn::Sub": "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}"
}
]
},
{
"Effect": "Allow",
"Action": [
"execute-api:*"
],
"Resource": [
{
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${API}/*/*/*"
}
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
{
"Fn::Sub": "arn:aws:s3:::${ImportBucket}/data/*"
},
{
"Fn::Sub": "arn:aws:s3:::${TestAllBucket}/data/*"
}
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
{
"Fn::Sub": "arn:aws:s3:::${ExportBucket}/data/*"
},
{
"Fn::Sub": "arn:aws:s3:::${ContentDesignerOutputBucket}/data-testall/*"
},
{
"Fn::Sub": "arn:aws:s3:::${ContentDesignerOutputBucket}/data-export/*"
}
]
},
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
{
"Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${SolutionHelper}"
}
]
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem"
],
"Resource": [
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SettingsTable}"
}
]
}
]
}
}
]
},
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "F3",
"reason": "This role policy is required to have * action in its policy"
}
]
},
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK", "CFN_NO_EXPLICIT_RESOURCE_NAMES"] }
}
},
"UserRole": {
"Type": "AWS::IAM::Role",
"Metadata": {
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK", "CFN_NO_EXPLICIT_RESOURCE_NAMES"] }
},
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "IdPool"
}
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
},
"Path": "/",
"RoleName": {
"Fn::Join": [
"",
[
{
"Fn::Select": ["0",
{
"Fn::Split": ["-",
{
"Fn::Select" : [2,
{
"Fn::Split": ["/",
{
"Ref": "AWS::StackId"
}
]
}
]
}]
}]
},
"-UserRole"
]
]},
"Policies": [
{
"Fn::If": [
"StreamingEnabled",
{
"PolicyName": "StreamingApiAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"execute-api:Invoke"
],
"Resource": [
{
"Fn::Join": ["", [
"arn:",
{ "Fn::Sub": "${AWS::Partition}" },
":execute-api:",
{ "Fn::Sub": "${AWS::Region}" },
":",
{ "Fn::Sub": "${AWS::AccountId}" },
":",
{ "Fn::GetAtt": ["StreamingStack", "Outputs.StreamingWebSocketApiId"] },
"/Prod/*"
]]
}
]
}
]
}
},
{
"Ref": "AWS::NoValue"
}
]
}
]
}
},
"UnauthenticatedRole": {
"Type": "AWS::IAM::Role",
"Metadata": {
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK","CFN_NO_EXPLICIT_RESOURCE_NAMES"] }
},
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": {
"Ref": "IdPool"
}
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "unauthenticated"
}
}
}
]
},
"Path": "/",
"RoleName": {
"Fn::Join": [
"",
[
{
"Fn::Select": ["0",
{
"Fn::Split": ["-",
{
"Fn::Select" : [2,
{
"Fn::Split": ["/",
{
"Ref": "AWS::StackId"
}
]
}
]
}]
}]
},
"-UnauthenticatedRole"
]
]}
}
},
"CFNLambdaRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Path": "/",
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
"arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
],
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":logs:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":log-group:/aws/lambda/*"
]
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "LambdaFunctionServiceRolePolicy"
},
{
"PolicyName": "CFNAccess",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"es:ESHttp*",
"es:UpdateDomainConfig",
"es:DescribeDomain",
"es:DescribeDomains",
"es:DescribeDomainConfig"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:es:${AWS::Region}:${AWS::AccountId}:domain*"
}
]
},
{
"Effect": "Allow",
"Action": [
"lex:PutSlotType",
"lex:GetSlotType",
"lex:DeleteSlotType",
"lex:PutIntent",
"lex:GetIntent",
"lex:DeleteIntent",
"lex:PutBot",
"lex:GetBot",
"lex:DeleteBot",
"lex:PutBotAlias",
"lex:DeleteBotAlias",
"lex:GetBotAlias",
"lex:GetBotVersions",
"lex:GetIntentVersions",
"lex:GetSlotTypeVersions"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"apigateway:*"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"bedrock:GetInferenceProfile",
"bedrock:GetFoundationModel"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":role/",
{
"Fn::Select": ["0",
{
"Fn::Split": ["-",
{
"Fn::Select" : [2,
{
"Fn::Split": ["/",
{
"Ref": "AWS::StackId"
}
]
}
]
}]
}]
},
"-*"
]
]
}
},
{
"Effect": "Allow",
"Action": [
"cognito-identity:SetIdentityPoolRoles",
"cognito-identity:GetIdentityPoolRoles",
"iam:CreateServiceLinkedRole"
],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": [
"cognito-idp:*"
],
"Resource": ["*"]
},
{
"Sid": "CFNLambdaS3Access",
"Effect": "Allow",
"Action": [
"s3:ListBucketVersions",
"s3:PutBucketNotification",
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObjectVersion",
"s3:DeleteObject",
"s3:GetObjectVersion",
"s3:ListBucket"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:s3:::*"
}
]
},
{
"Effect": "Allow",
"Action": [
"lambda:PublishVersion"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*"
}
]
}
]
}
},
{
"PolicyName": "LambdaFunctionCustomResourcePollingPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:AddPermission",
"lambda:RemovePermission"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*"
}
]
},
{
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:DeleteRule",
"events:PutTargets",
"events:RemoveTargets"
],
"Resource": [
{
"Fn::Sub": "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/*"
}
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutBucketVersioning"
],
"Resource": [
{ "Fn::Sub": "arn:${AWS::Partition}:s3:::*" }
]
}
]
}
},
{
"PolicyName": "SettingsInitializerCustomResourcePolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:Scan",
"dynamodb:GetItem",
"dynamodb:UpdateItem"
],
"Resource": [
{
"Fn::GetAtt": ["SettingsTable","Arn"]
}
]
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter"
],
"Resource": [
{"Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${CustomQnABotSettings}" },
{"Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${PrivateQnABotSettings}" },
{"Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${DefaultQnABotSettings}" }
]
}
]
}
}
]
},
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "F3",
"reason": "This role policy is required to have * action in its policy"
},
{
"id": "F38",
"reason": "This role policy is required to have * action in its policy with PassRole action"
},
{
"id": "W11",
"reason": "This IAM role requires to have * resource on its permission policy"
}
]
},
"guard": { "SuppressedRules": ["IAM_NO_INLINE_POLICY_CHECK"] }
}
}
}
================================================
FILE: source/templates/master/routes/README.md
================================================
# ApiGateway
Apigateway routes
================================================
FILE: source/templates/master/routes/bot/alexa.vm
================================================
#set($inputRoot = $input.path('$'))
#set($utterances = $inputRoot.utterances)
{
"interactionModel": {
"languageModel": {
"invocationName": "q and a",
"types": [
{
"name": "EXAMPLE_QUESTIONS",
"values": [
#foreach( $utterance in $utterances)
{"name":{
"value":"$utterance"
}}#if( $foreach.hasNext ),#end
#end
]
}
## {
## "name": "EXAMPLE_QUESTIONS",
## "values": [
## {
## "name": {
## "value": "this is required"
## }
## }
## ]
## }
],
"intents": [
{
"slots": [
{
"name": "QnA_slot",
"type": "EXAMPLE_QUESTIONS"
}
],
"name": "Qna_intent",
"samples": [
"{QnA_slot}"
]
},
{
"name": "AMAZON.StopIntent"
},
{
"name": "AMAZON.RepeatIntent"
},
{
"name": "AMAZON.FallbackIntent"
},
{
"name": "AMAZON.CancelIntent"
}
]
}
}
}
================================================
FILE: source/templates/master/routes/bot/get.resp.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
#set($inputRoot = $input.path('$'))
{
"lambdaArn": "$inputRoot.lambdaArn",
"lambdaRole":"$inputRoot.lambdaRole",
"botversion":"$inputRoot.botversion",
"botname":"$inputRoot.botname",
"intent":"$inputRoot.intent",
"intentFallback":"$inputRoot.intentFallback",
"lexV2botname":"$inputRoot.lexV2botname",
"lexV2botid":"$inputRoot.lexV2botid",
"lexV2botalias":"$inputRoot.lexV2botalias",
"lexV2botaliasid":"$inputRoot.lexV2botaliasid",
"lexV2intent":"$inputRoot.lexV2intent",
"lexV2intentFallback":"$inputRoot.lexV2intentFallback",
"lexV2localeids":"$inputRoot.lexV2localeids",
"status":"$inputRoot.status",
"build":$input.json('$.build'),
"_links":{
"alexa":{
"href":"$root/bot/alexa"
}
}
}
================================================
FILE: source/templates/master/routes/bot/get.vm
================================================
{
"fnc":"getBot"
}
================================================
FILE: source/templates/master/routes/bot/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const resource = require('../util/resource');
const lambda = require('../util/lambda');
module.exports = {
Bot: resource('bot'),
AlexaApi: resource('alexa', { Ref: 'Bot' }),
AlexaSchema: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['UtteranceLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/utterance.get.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/alexa.vm`, 'utf8'),
resource: { Ref: 'AlexaApi' },
}),
BotPost: lambda({
authorization: 'AWS_IAM',
method: 'post',
lambda: { 'Fn::GetAtt': ['LexBuildLambdaStart', 'Arn'] },
resource: { Ref: 'Bot' },
responseTemplate: fs.readFileSync(`${__dirname}/post.resp.vm`, 'utf8'),
}),
BotGet: lambda({
authorization: 'AWS_IAM',
method: 'get',
subTemplate: fs.readFileSync(`${__dirname}/get.vm`, 'utf8'),
lambda: { 'Fn::GetAtt': ['LexStatusLambda', 'Arn'] },
resource: { Ref: 'Bot' },
responseTemplate: fs.readFileSync(`${__dirname}/get.resp.vm`, 'utf8'),
}),
BotDoc: {
Type: 'AWS::ApiGateway::DocumentationPart',
Properties: {
Location: {
Type: 'RESOURCE',
Path: '/bot',
},
Properties: JSON.stringify({
description: '',
}),
RestApiId: { Ref: 'API' },
},
},
};
================================================
FILE: source/templates/master/routes/bot/post.resp.vm
================================================
{"token":"$input.path('$.token')"}
================================================
FILE: source/templates/master/routes/bot/post.vm
================================================
{
}
================================================
FILE: source/templates/master/routes/bot/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
process.argv.push('--debug');
const Velocity = require('velocity');
const { run } = require('../util/temp-test');
const { input } = require('../util/temp-test');
module.exports = {
get: (test) => run(`${__dirname}/get`, {}, test),
getresp: (test) => run(`${__dirname}/get.resp`, input({
status: 'BUILDING',
build: { test: 'a' },
abortStatement: {
messages: [
{ content: '2' },
{ content: '3' },
],
},
clarificationPrompt: {
messages: [
{ content: '1' },
{ content: '4' },
],
},
}), test),
post: (test) => run(`${__dirname}/` + 'post', {}, test),
resp: (test) => run(`${__dirname}/` + 'post.resp', {}, test),
utterance: {
get: (test) => run(`${__dirname}/` + 'utterance.get', {}, test),
alexa: (test) => run(`${__dirname}/` + 'alexa', {
input: {
path() {
return {
enumerationValues: [
{ value: 'thin, or thin' },
{ value: 'thick' },
],
};
},
},
}, test),
},
};
================================================
FILE: source/templates/master/routes/bot/utterance.get.vm
================================================
{
}
================================================
FILE: source/templates/master/routes/error/error.vm
================================================
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"type":"$errorMessageObj.type",
"message":"$errorMessageObj.message",
"data":"$errorMessageObj.data"
}
================================================
FILE: source/templates/master/routes/error/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
const { input } = require('../util/temp-test');
module.exports = {
error: {
get: (test) => run(
`${__dirname}/` + 'error',
input({
errorMessage: JSON.stringify(
{ status: 404, message: 'aaa' },
),
}),
test,
),
},
};
================================================
FILE: source/templates/master/routes/examples/handler.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, ListObjectsCommand } = require('@aws-sdk/client-s3');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const s3 = new S3Client(customSdkConfig('C018', { region }));
exports.photos = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
const result = await s3.send(new ListObjectsCommand({
Bucket: event.bucket,
Prefix: event.prefix,
MaxKeys: event.perpage || 100,
Marker: event.token || null,
}));
console.log('s3 response for photos:', JSON.stringify(result, null, 2));
const photos = result?.Contents?.map((value) => {
const key = value.Key.split('/').pop();
return `${event.root}/examples/photos/${key}`;
}, []);
return {
token: result.NextMarker,
photos,
};
} catch (error) {
throw JSON.stringify({
type: '[InternalServiceError]',
data: error,
});
}
};
exports.documents = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
const result = await s3.send(new ListObjectsCommand({
Bucket: event.bucket,
Prefix: event.prefix,
MaxKeys: event.perpage || 100,
Marker: event.token || null,
}));
console.log('s3 response for documents:', JSON.stringify(result, null, 2));
const examples = result?.Contents?.reduce((accum, value) => {
let key = value.Key.split('/').pop().split('.');
const ext = key.length > 1 ? key.pop() : 'txt';
key = key[0];
const href = `${event.root}/examples/documents/${key}.${ext}`;
if (!accum[key]) {
accum[key] = { id: key };
}
if (ext === 'json') {
accum[key].document = { href };
} else {
accum[key].description = { href };
}
return accum;
}, {});
return {
token: result.NextMarker,
examples: examples ? Object.keys(examples).map((x) => examples[x]) : [],
};
} catch (error) {
throw JSON.stringify({
type: '[InternalServiceError]',
data: error,
});
}
};
================================================
FILE: source/templates/master/routes/examples/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const resource = require('../util/resource');
const lambda = require('../util/lambda');
const mock = require('../util/mock');
const util = require('../../../util');
module.exports = {
Examples: resource('examples'),
ExamplesGet: mock({
authorization: 'AWS_IAM',
method: 'GET',
subTemplate: 'examples/info',
resource: { Ref: 'Examples' },
}),
photos: resource('photos', { Ref: 'Examples' }),
photosList: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['ExampleS3ListPhotoLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/photos.vm`, 'utf8'),
resource: { Ref: 'photos' },
parameterLocations: {
'method.request.querystring.perpage': false,
'method.request.querystring.token': false,
},
}),
photo: resource('{proxy+}', { Ref: 'photos' }),
photoGet: proxy({
resource: { Ref: 'photo' },
method: 'get',
bucket: { Ref: 'AssetBucket' },
path: '/examples/photos/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
authorization: 'AWS_IAM',
}),
Documents: resource('documents', { Ref: 'Examples' }),
DocumentsList: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['ExampleS3ListLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/list.vm`, 'utf8'),
resource: { Ref: 'Documents' },
parameterLocations: {
'method.request.querystring.perpage': false,
'method.request.querystring.token': false,
},
}),
Example: resource('{proxy+}', { Ref: 'Documents' }),
ExampleGet: proxy({
authorization: 'AWS_IAM',
resource: { Ref: 'Example' },
method: 'get',
bucket: { Ref: 'AssetBucket' },
path: '/examples/documents/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
ExampleHead: proxy({
resource: { Ref: 'Example' },
method: 'head',
bucket: { Ref: 'AssetBucket' },
path: '/examples/documents/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
authorization: 'AWS_IAM',
}),
ExampleS3ListLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ExampleS3ListLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ExampleS3ListLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf8'),
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables()
}
},
Handler: 'index.documents',
LoggingConfig: {
LogGroup: { Ref: 'ExampleS3ListLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['S3ListLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
ExampleS3ListPhotoLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-ExampleS3ListPhotoLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
ExampleS3ListPhotoLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf8'),
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables()
}
},
Handler: 'index.photos',
LoggingConfig: {
LogGroup: { Ref: 'ExampleS3ListPhotoLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['S3ListLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
};
function proxy(opts) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: opts.auth || 'AWS_IAM',
HttpMethod: opts.method.toUpperCase(),
Integration: {
Type: 'AWS',
IntegrationHttpMethod: opts.method.toUpperCase(),
Credentials: { 'Fn::GetAtt': ['S3AccessRole', 'Arn'] },
Uri: {
'Fn::Join': ['', [
'arn:aws:apigateway:',
{ Ref: 'AWS::Region' },
':s3:path/', opts.bucket,
opts.path,
]],
},
RequestParameters: opts.requestParams || {},
IntegrationResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': 'integration.response.header.Content-Type',
...opts.responseParameters,
},
}, {
StatusCode: 404,
ResponseTemplates: {
'application/xml': JSON.stringify({
error: opts.missingMessage || 'Not Found',
}),
},
SelectionPattern: '403',
},
],
},
RequestParameters: {
'method.request.path.proxy': false,
},
ResourceId: opts.resource,
MethodResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': false,
..._.mapValues(opts.responseParameters || {}, (x) => false),
},
},
{ StatusCode: 400 },
{ StatusCode: 404 },
],
RestApiId: { Ref: 'API' },
},
};
}
================================================
FILE: source/templates/master/routes/examples/info.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"_links":{
"documents":{
"href":"$root/examples/documents"
},
"photos":{
"href":"$root/examples/photos"
}
}
}
================================================
FILE: source/templates/master/routes/examples/list.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"bucket":"${AssetBucket}",
"prefix":"examples/documents/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"root":"$root"
}
================================================
FILE: source/templates/master/routes/examples/photos.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"bucket":"${AssetBucket}",
"prefix":"examples/photos/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"root":"$root"
}
================================================
FILE: source/templates/master/routes/examples/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
const { input } = require('../util/temp-test');
module.exports = {
list: (test) => run(`${__dirname}/` + 'list', input({
perpage: 100,
token: '',
}), test),
list: (test) => run(`${__dirname}/` + 'photos', input({
perpage: 100,
token: '',
}), test),
async handler(test) {
const output = await require('../../../../bin/exports')('dev/bucket');
try {
require('./handler').documents({
bucket: output.Bucket,
prefix: '',
root: 'example.com',
}, {}, (err, result) => {
console.log(result);
test.ifError(err);
test.ok(result);
test.done();
});
} catch (e) {
test.ifError(e);
test.done();
}
},
async handlerPhoto(test) {
const output = await require('../../../../bin/exports')('dev/bucket');
try {
require('./handler').photos({
bucket: output.Bucket,
prefix: '',
root: 'example.com',
}, {}, (err, result) => {
console.log(result);
test.ifError(err);
test.ok(result);
test.done();
});
} catch (e) {
test.ifError(e);
test.done();
}
},
};
================================================
FILE: source/templates/master/routes/health/health.resp.vm
================================================
{"status":"health"}
================================================
FILE: source/templates/master/routes/health/health.vm
================================================
{
"endpoint":"${ESVar.ESAddress}",
"method":"GET",
"path":"/_cluster/health"
}
================================================
FILE: source/templates/master/routes/health/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const resource = require('../util/resource');
const lambda = require('../util/lambda');
module.exports = {
Health: resource('health'),
HealthGet: lambda({
method: 'get',
authorization: 'AWS_IAM',
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/health.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/health.resp.vm`, 'utf8'),
resource: { Ref: 'Health' },
}),
};
================================================
FILE: source/templates/master/routes/health/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
module.exports = {
health: {
get: (test) => run(`${__dirname}/` + 'health', {}, test),
resp: (test) => run(`${__dirname}/` + 'health.resp', {}, test),
},
};
================================================
FILE: source/templates/master/routes/images.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const resource = require('./util/resource');
const util = require('../../util');
module.exports = {
Images: resource('images'),
ImagesProxy: resource('{proxy+}', { Ref: 'Images' }),
ImagesProxyGet: proxy({
auth: 'NONE',
resource: { Ref: 'ImagesProxy' },
method: 'get',
path: '/assets/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
responseParameters: {
'method.response.header.api-stage': 'context.stage',
},
}),
};
function proxy(opts) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: opts.auth || 'AWS_IAM',
HttpMethod: opts.method.toUpperCase(),
Integration: {
Type: 'AWS',
IntegrationHttpMethod: opts.method.toUpperCase(),
Credentials: { 'Fn::GetAtt': ['S3AccessRole', 'Arn'] },
Uri: {
'Fn::Join': ['', [
'arn:aws:apigateway:',
{ Ref: 'AWS::Region' },
':s3:path/', { Ref: 'Bucket' },
opts.path,
]],
},
RequestParameters: opts.requestParams || {},
IntegrationResponses: [
{
StatusCode: 200,
ContentHandling: 'CONVERT_TO_BINARY',
ResponseParameters: {
'method.response.header.content-type': 'integration.response.header.Content-Type',
...opts.responseParameters,
},
}, {
StatusCode: 404,
ResponseTemplates: {
'application/xml': JSON.stringify({
error: 'Not found',
}),
},
SelectionPattern: '403',
},
],
},
RequestParameters: {
'method.request.path.proxy': false,
},
ResourceId: opts.resource,
MethodResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': false,
..._.mapValues(opts.responseParameters || {}, (x) => false),
},
},
{ StatusCode: 400 },
{ StatusCode: 404 },
],
RestApiId: { Ref: 'API' },
},
Metadata: { cfn_nag: util.cfnNag(['W59']) },
};
}
================================================
FILE: source/templates/master/routes/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = Object.assign(
require('./bot'),
require('./health'),
require('./root'),
require('./qa'),
require('./proxy'),
require('./login'),
require('./jobs'),
require('./examples'),
require('./services'),
require('./images'),
);
================================================
FILE: source/templates/master/routes/jobs/__tests__/handler.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require('aws-sdk-client-mock-jest');
const { S3Client, ListObjectsCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
const { handler } = require('../handler');
describe('lex poll', () => {
beforeEach(() => {
jest.resetModules();
s3ClientMock.reset();
});
it('returns sorted list of routes', async () => {
const event = {
bucket: 'test-bucket',
prefix: 'test-prefix',
perpage: 10,
token: 'test-token',
root: 'http://localhost',
type: 'test',
};
s3ClientMock.on(ListObjectsCommand).resolves({
Contents: [
{
LastModified: '2019-11-01T23:11:50.000Z',
Key: 'doc2.rtf',
},
{
LastModified: '2019-11-03T23:11:50.000Z',
Key: 'doc4.rtf',
},
{
LastModified: '2019-11-02T23:11:50.000Z',
Key: 'doc3.rtf',
},
{
Key: 'doc1.rtf',
},
],
});
const result = await handler(event, {});
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, ListObjectsCommand, {
Bucket: 'test-bucket',
Marker: 'test-token',
Prefix: 'test-prefix',
MaxKeys: 10,
});
expect(result).toEqual({
jobs: [
{
id: 'doc4.rtf',
href: 'http://localhost/jobs/test/doc4.rtf',
},
{
id: 'doc3.rtf',
href: 'http://localhost/jobs/test/doc3.rtf',
},
{
id: 'doc2.rtf',
href: 'http://localhost/jobs/test/doc2.rtf',
},
{
id: 'doc1.rtf',
href: 'http://localhost/jobs/test/doc1.rtf',
},
],
});
});
it('returns sorted list of routes using default event params', async () => {
const event = {
bucket: 'test-bucket',
prefix: 'test-prefix',
root: 'http://localhost',
type: 'test',
};
s3ClientMock.on(ListObjectsCommand).resolves({
Contents: [
{
LastModified: '2019-11-01T23:11:50.000Z',
Key: 'doc2.rtf',
},
{
LastModified: '2019-11-03T23:11:50.000Z',
Key: 'doc4.rtf',
},
{
LastModified: '2019-11-02T23:11:50.000Z',
Key: 'doc3.rtf',
},
{
Key: 'doc1.rtf',
},
],
});
const result = await handler(event, {});
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, ListObjectsCommand, {
Bucket: 'test-bucket',
Marker: null,
Prefix: 'test-prefix',
MaxKeys: 100,
});
expect(result).toEqual({
jobs: [
{
id: 'doc4.rtf',
href: 'http://localhost/jobs/test/doc4.rtf',
},
{
id: 'doc3.rtf',
href: 'http://localhost/jobs/test/doc3.rtf',
},
{
id: 'doc2.rtf',
href: 'http://localhost/jobs/test/doc2.rtf',
},
{
id: 'doc1.rtf',
href: 'http://localhost/jobs/test/doc1.rtf',
},
],
});
});
it('handles errors from s3', async () => {
const event = {
bucket: 'test-bucket',
prefix: 'test-prefix',
perpage: 10,
token: 'test-token',
root: 'http://localhost',
type: 'test',
};
s3ClientMock.on(ListObjectsCommand).rejects('mocked rejection');
await expect(handler(event, {})).rejects.toEqual(JSON.stringify({
type: '[InternalServiceError]',
data: {},
}));
expect(s3ClientMock).toHaveReceivedNthCommandWith(1, ListObjectsCommand, {
Bucket: 'test-bucket',
Marker: 'test-token',
Prefix: 'test-prefix',
MaxKeys: 10,
});
});
});
================================================
FILE: source/templates/master/routes/jobs/export-start.vm
================================================
#set($inputRoot = $input.path('$'))
{
"bucket":"${ExportBucket}",
"index":"${Var.QnaIndex}",
"id":"$input.params('proxy')",
"config":"status/$input.params('proxy')",
"tmp":"tmp/$input.params('proxy')",
"key":"$inputRoot.get('prefix')data-export/$input.params('proxy')",
"filter":"$inputRoot.get('filter')",
"status":"Started"
}
================================================
FILE: source/templates/master/routes/jobs/handler.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, ListObjectsCommand } = require('@aws-sdk/client-s3');
const customSdkConfig = require('sdk-config/customSdkConfig');
const region = process.env.AWS_REGION;
const s3 = new S3Client(customSdkConfig('C022', { region }));
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
const result = await s3.send(new ListObjectsCommand({
Bucket: event.bucket,
Prefix: event.prefix,
MaxKeys: event.perpage || 100,
Marker: event.token || null,
}));
console.log('s3 response for routes:', JSON.stringify(result, null, 2));
if (result.Contents) {
result.Contents?.sort((a, b) => {
if (a.LastModified && b.LastModified) {
return new Date(b.LastModified).getTime() - new Date(a.LastModified).getTime();
}
return 0;
});
}
const mapJobs = result?.Contents?.map((y) => ({
id: y.Key.split('/').pop(),
href: `${event.root}/jobs/${event.type}/${encodeURI(y.Key.split('/').pop())}`,
}));
return {
token: result.NextMarker,
jobs: result.Contents ? mapJobs : [],
};
} catch (error) {
throw JSON.stringify({
type: '[InternalServiceError]',
data: error,
});
}
};
================================================
FILE: source/templates/master/routes/jobs/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const resource = require('../util/resource');
const lambda = require('../util/lambda');
const mock = require('../util/mock');
const util = require('../../../util');
module.exports = {
Jobs: resource('jobs'),
JobsGet: mock({
auth: 'AWS_IAM',
method: 'GET',
subTemplate: 'jobs/info',
resource: { Ref: 'Jobs' },
}),
testalls: resource('testall', { Ref: 'Jobs' }),
testallsList: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['S3ListLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/list-testall.vm`, 'utf8'),
resource: { Ref: 'testalls' },
parameterLocations: {
'method.request.querystring.perpage': false,
'method.request.querystring.token': false,
},
}),
testall: resource('{proxy+}', { Ref: 'testalls' }),
testallPut: proxy({
resource: { Ref: 'testall' },
auth: 'AWS_IAM',
method: 'PUT',
bucket: { Ref: 'TestAllBucket' },
path: '/status-testall/{proxy}',
template: fs.readFileSync(`${__dirname}/testall-start.vm`, 'utf-8'),
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
testallGet: proxy({
resource: { Ref: 'testall' },
auth: 'AWS_IAM',
method: 'GET',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status-testall/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
testallDelete: proxy({
resource: { Ref: 'testall' },
auth: 'AWS_IAM',
method: 'delete',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status-testall/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
exports: resource('exports', { Ref: 'Jobs' }),
exportsList: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['S3ListLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/list-export.vm`, 'utf8'),
resource: { Ref: 'exports' },
parameterLocations: {
'method.request.querystring.perpage': false,
'method.request.querystring.token': false,
},
}),
export: resource('{proxy+}', { Ref: 'exports' }),
imports: resource('imports', { Ref: 'Jobs' }),
exportPut: proxy({
resource: { Ref: 'export' },
auth: 'AWS_IAM',
method: 'PUT',
bucket: { Ref: 'ExportBucket' },
path: '/status-export/{proxy}',
template: fs.readFileSync(`${__dirname}/export-start.vm`, 'utf-8'),
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
exportGet: proxy({
resource: { Ref: 'export' },
auth: 'AWS_IAM',
method: 'GET',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status-export/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
exportDelete: proxy({
resource: { Ref: 'export' },
auth: 'AWS_IAM',
method: 'delete',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
importsList: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['S3ListLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/list.vm`, 'utf8'),
resource: { Ref: 'imports' },
parameterLocations: {
'method.request.querystring.perpage': false,
'method.request.querystring.token': false,
},
}),
import: resource('{proxy+}', { Ref: 'imports' }),
importGet: proxy({
resource: { Ref: 'import' },
auth: 'AWS_IAM',
method: 'get',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status-import/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
importDelete: proxy({
resource: { Ref: 'import' },
auth: 'AWS_IAM',
method: 'delete',
bucket: { Ref: 'ContentDesignerOutputBucket' },
path: '/status-import/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
}),
S3ListLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-S3ListLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
S3ListLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/handler.js`, 'utf8'),
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables()
}
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'S3ListLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['S3ListLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
S3ListLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'S3ListPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['S3:List*'],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:s3:::*' }],
},
],
},
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
function proxy(opts) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: opts.auth || 'AWS_IAM',
HttpMethod: opts.method.toUpperCase(),
Integration: _.pickBy({
Type: 'AWS',
IntegrationHttpMethod: opts.method.toUpperCase(),
Credentials: { 'Fn::GetAtt': ['S3AccessRole', 'Arn'] },
Uri: {
'Fn::Join': ['', [
'arn:aws:apigateway:',
{ Ref: 'AWS::Region' },
':s3:path/', opts.bucket,
opts.path,
]],
},
RequestParameters: opts.requestParams || {},
RequestTemplates: opts.template ? {
'application/json': { 'Fn::Sub': opts.template },
} : null,
IntegrationResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': 'integration.response.header.Content-Type',
...opts.responseParameters,
},
}, {
StatusCode: 404,
ResponseTemplates: {
'application/xml': JSON.stringify({
error: 'Job not found',
}),
},
SelectionPattern: '403',
},
],
}),
RequestParameters: {
'method.request.path.proxy': false,
},
ResourceId: opts.resource,
MethodResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': false,
..._.mapValues(opts.responseParameters || {}, (x) => false),
},
},
{ StatusCode: 400 },
{ StatusCode: 404 },
],
RestApiId: { Ref: 'API' },
},
};
}
================================================
FILE: source/templates/master/routes/jobs/info.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"_links":{
"imports":{
"href":"$root/jobs/imports",
"bucket":"${ImportBucket}",
"uploadPrefix":"data/",
"statusPrefix":"Status/"
},
"exports":{
"href":"$root/jobs/exports"
},
"testall":{
"href":"$root/jobs/testall",
"bucket":"${TestAllBucket}",
"statusPrefix":"Status/"
}
}
}
================================================
FILE: source/templates/master/routes/jobs/list-export.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"bucket":"${ContentDesignerOutputBucket}",
"prefix":"status-export/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"exports",
"root":"$root"
}
================================================
FILE: source/templates/master/routes/jobs/list-testall.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"bucket":"${ContentDesignerOutputBucket}",
"prefix":"status-testall/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"testall",
"root":"$root"
}
================================================
FILE: source/templates/master/routes/jobs/list.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"bucket":"${ContentDesignerOutputBucket}",
"prefix":"status-import/",
"perpage":"$input.params('perpage')",
"token":"$input.params('token')",
"type":"imports",
"root":"$root"
}
================================================
FILE: source/templates/master/routes/jobs/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
const { input } = require('../util/temp-test');
module.exports = {
info: (test) => run(`${__dirname}/` + 'info', {}, test),
start: (test) => run(`${__dirname}/` + 'export-start', {}, test),
listExports: (test) => run(`${__dirname}/` + 'list-export', {
perpage: 100,
token: '',
}, test),
list: (test) => run(`${__dirname}/` + 'list', input({
perpage: 100,
token: '',
}), test),
};
================================================
FILE: source/templates/master/routes/jobs/testall-start.vm
================================================
#set($inputRoot = $input.path('$'))
{
"bucket":"${TestAllBucket}",
"index":"${Var.QnaIndex}",
"id":"$input.params('proxy')",
"config":"status-testall/$input.params('proxy')",
"tmp":"tmp-testall/$input.params('proxy')",
"key":"data-testall/$input.params('proxy')",
"filter":"$inputRoot.get('filter')",
"token":"$inputRoot.get('token')",
"locale":"$inputRoot.get('locale')",
"status":"Started"
}
================================================
FILE: source/templates/master/routes/login.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const resource = require('./util/resource');
const redirect = require('./util/redirect');
module.exports = {
Login: resource('pages'),
DesignerLoginResource: resource('designer', { Ref: 'Login' }),
ClientLoginResource: resource('client', { Ref: 'Login' }),
DesignerLoginResourceGet: redirect(
{ 'Fn::GetAtt': ['DesignerLogin', 'loginUrl'] },
{ Ref: 'DesignerLoginResource' },
),
ClientLoginResourceGet: redirect(
{ 'Fn::GetAtt': ['ClientLogin', 'loginUrl'] },
{ Ref: 'ClientLoginResource' },
),
};
================================================
FILE: source/templates/master/routes/proxy.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const resource = require('./util/resource');
const util = require('../../util');
module.exports = {
Static: resource('static'),
Proxy: resource('{proxy+}', { Ref: 'Static' }),
ProxyAnyGet: proxy({
auth: 'NONE',
resource: { Ref: 'Proxy' },
method: 'get',
path: '/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
responseParameters: {
'method.response.header.api-stage': 'context.stage',
},
}),
ProxyAnyHead: proxy({
auth: 'NONE',
resource: { Ref: 'Proxy' },
method: 'head',
path: '/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
responseParameters: {
'method.response.header.api-stage': 'context.stage',
},
}),
Fonts: resource('fonts', { Ref: 'Static' }),
FontsProxy: resource('{proxy+}', { Ref: 'Fonts' }),
FontsProxyGet: proxy({
auth: 'NONE',
resource: { Ref: 'FontsProxy' },
method: 'get',
path: '/fonts/{proxy}',
requestParams: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
responseParameters: {
'method.response.header.api-stage': 'context.stage',
},
contentHandling: 'CONVERT_TO_BINARY',
}),
};
function proxy(opts) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: opts.auth || 'AWS_IAM',
HttpMethod: opts.method.toUpperCase(),
Integration: {
Type: 'AWS',
IntegrationHttpMethod: opts.method.toUpperCase(),
Credentials: { 'Fn::GetAtt': ['S3AccessRole', 'Arn'] },
Uri: {
'Fn::Join': ['', [
'arn:aws:apigateway:',
{ Ref: 'AWS::Region' },
':s3:path/', { Ref: 'Bucket' },
opts.path,
]],
},
RequestParameters: opts.requestParams || {},
IntegrationResponses: [
{
StatusCode: 200,
ContentHandling: opts.contentHandling,
ResponseParameters: {
'method.response.header.content-type': 'integration.response.header.Content-Type',
...opts.responseParameters,
},
}, {
StatusCode: 404,
ResponseTemplates: {
'application/xml': JSON.stringify({
error: 'Not found',
}),
},
SelectionPattern: '403',
},
],
},
RequestParameters: {
'method.request.path.proxy': false,
},
ResourceId: opts.resource,
MethodResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.content-type': false,
..._.mapValues(opts.responseParameters || {}, (x) => false),
},
},
{ StatusCode: 400 },
{ StatusCode: 404 },
],
RestApiId: { Ref: 'API' },
},
Metadata: { cfn_nag: util.cfnNag(['W59']) },
};
}
================================================
FILE: source/templates/master/routes/qa/collection/delete.resp.vm
================================================
{
"message":"success",
"count":"$input.path('$.deleted')"
}
================================================
FILE: source/templates/master/routes/qa/collection/delete.vm
================================================
{
"endpoint":"${ESVar.ESAddress}",
"method":"POST",
"path":"/${Var.QnaIndex}/_delete_by_query?refresh=true",
"body":{
"query":{
#if($input.path('$.query').length()!=0)
"bool":{
"must":{"match_all":{}},
"filter":{"regexp":{
"qid":"$input.path('$.query')"
}}
}
#else
"terms":{
"qid":[
#foreach($qid in $input.path('$.list'))
"$qid"#if($foreach.hasNext),#end
#end]
}
#end
}
}
}
================================================
FILE: source/templates/master/routes/qa/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const resource = require('../util/resource');
const lambda = require('../util/lambda');
module.exports = {
Questions: resource('questions'),
QuestionsGet: lambda({
authorization: 'AWS_IAM',
method: 'get',
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/single/get.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/single/get.resp.vm`, 'utf8'),
resource: { Ref: 'Questions' },
parameterLocations: {
'method.request.querystring.query': false,
'method.request.querystring.topic': false,
'method.request.querystring.from': false,
'method.request.querystring.filter': false,
'method.request.querystring.order': false,
'method.request.querystring.perpage': false,
},
}),
QuestionsDelete: lambda({
authorization: 'AWS_IAM',
method: 'delete',
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/collection/delete.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/collection/delete.resp.vm`, 'utf8'),
defaultResponse: 204,
resource: { Ref: 'Questions' },
}),
Question: resource('{ID}', { Ref: 'Questions' }),
QuestionHead: lambda({
authorization: 'AWS_IAM',
method: 'head',
errors: [{
SelectionPattern: '.*status":404.*',
StatusCode: 404,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
}],
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/single/head.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/single/head.resp.vm`, 'utf8'),
resource: { Ref: 'Question' },
parameterLocations: {
'method.request.path.Id': true,
},
}),
QuestionPut: lambda({
authorization: 'AWS_IAM',
method: 'put',
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/single/put.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/single/put.resp.vm`, 'utf8'),
resource: { Ref: 'Question' },
parameterLocations: {
'method.request.path.Id': true,
},
defaultResponse: 201,
}),
QuestionsOptions: lambda({
authorization: 'AWS_IAM',
method: 'options',
lambda: { 'Fn::GetAtt': ['SchemaLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/single/options.vm`, 'utf8'),
resource: { Ref: 'Questions' },
}),
QuestionDelete: lambda({
authorization: 'AWS_IAM',
method: 'delete',
lambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
subTemplate: fs.readFileSync(`${__dirname}/single/delete.vm`, 'utf8'),
responseTemplate: fs.readFileSync(`${__dirname}/single/delete.resp.vm`, 'utf8'),
resource: { Ref: 'Question' },
defaultResponse: 204,
parameterLocations: {
'method.request.path.Id': true,
},
}),
};
================================================
FILE: source/templates/master/routes/qa/single/delete.resp.vm
================================================
#set($inputRoot = $input.path('$'))
#set($Idpath = '$._id')
#set($Successpath = '$._shards.successful')
{
"result":"$inputRoot.result",
"id":$input.json($Idpath),
"success":$input.json($Successpath)
}
================================================
FILE: source/templates/master/routes/qa/single/delete.vm
================================================
{
"endpoint":"${ESVar.ESAddress}",
"method":"POST",
"path":"/${Var.QnaIndex}/_delete_by_query?refresh=true",
"body":{
"query":{
"match":{
"qid":"$util.urlDecode($input.params('ID'))"
}
}
}
}
================================================
FILE: source/templates/master/routes/qa/single/get.resp.vm
================================================
#set($inputRoot = $input.path('$'))
{
"total":$inputRoot.hits.total.value,
"version":"1",
"qa":[
#foreach( $hit in $inputRoot.hits.hits)
{
#set($Scorepath = '$.hits.hits['+$foreach.index+']._score')
"_score":$input.json($Scorepath),
#set($Bodypath = '$.hits.hits['+$foreach.index+']._source')
#foreach($paramName in $input.path($Bodypath).keySet())
#if( $paramName == 'questions')
"q":[
#foreach( $question in $input.path($Bodypath).get($paramName))
"$question.q"
#if($foreach.hasNext),#end
#end
]
#else
#set( $body = $Bodypath+"."+$paramName)
"$paramName" :$input.json($body)
#end
#if($foreach.hasNext),#end
#end
}#if( $foreach.hasNext ),#end
#end
]
}
================================================
FILE: source/templates/master/routes/qa/single/get.vm
================================================
#if ( $input.params('perpage').length()==0 )
#set ( $perpage = 10 )
#else
#set ( $perpage = $input.params('perpage') )
#end
#if ( $input.params('from').length()==0)
#set ( $from = 0 )
#else
#set ( $from = $input.params('from') )
#end
#if ( $input.params('order').length()==0 )
#set ( $order = "asc" )
#else
#set ( $order = $input.params('order') )
#end
{
"endpoint":"${ESVar.ESAddress}",
"method":"POST",
#if($input.params('query').length()>0)
"path":"/${Var.QnaIndex}/_search?search_type=dfs_query_then_fetch",
"question": "$util.urlDecode($input.params('query'))",
#else
"path":"/${Var.QnaIndex}/_search?search_type=dfs_query_then_fetch",
"question": "",
#end
#if ($input.params('topic'))
"topic": "$util.urlDecode($input.params('topic'))",
#else
"topic": "",
#end
#if ($input.params('client_filter'))
"client_filter": "$util.urlDecode($input.params('client_filter'))",
#else
"client_filter": "",
#end
#if ($input.params('score_answer'))
"score_answer": "$util.urlDecode($input.params('score_answer'))",
#else
"score_answer": "",
#end
#if ($input.params('score_text_passage'))
"score_text_passage": "$util.urlDecode($input.params('score_text_passage'))",
#else
"score_text_passage": "",
#end
"size":"$perpage",
"from":"$from",
"body":{
#if($input.params('query').length()>0)
"comment": "ES Query for test queries are now built dynamically by ESProxy Lambda handler."
#else
"size":"$perpage",
"from":"$from",
"_source": {
"exclude": ["questions.q_vector", "a_vector"]
},
"query": {
"bool":{
#if($input.params('filter').length()==0)
"must":{"match_all":{}}
#else
"filter":{"regexp":{
"qid":"$util.urlDecode($input.params('filter'))"
}}
#end
}
}
,"sort":{
"qid":{
"order":"$order"
}
}
#end
}
}
================================================
FILE: source/templates/master/routes/qa/single/head.resp.vm
================================================
{"status":"exists"}
================================================
FILE: source/templates/master/routes/qa/single/head.vm
================================================
{
"endpoint":"${ESVar.ESAddress}",
"method":"HEAD",
"path":"/${Var.QnaIndex}/_all/$util.urlDecode($input.params('ID'))"
}
================================================
FILE: source/templates/master/routes/qa/single/options.vm
================================================
{
"comment": "API mapping no-op since ES 7.x upgrade. Schema now returned directly from SchemaLambda, rather than from OpenSearch metadata"
}
================================================
FILE: source/templates/master/routes/qa/single/put.resp.vm
================================================
#set($inputRoot = $input.path('$'))
#set($Idpath = '$._id')
#set($Successpath = '$._shards.successful')
{
"result":"$inputRoot.result",
"id":$input.json($Idpath),
"success":$input.json($Successpath)
}
================================================
FILE: source/templates/master/routes/qa/single/put.vm
================================================
#set($inputRoot = $input.path('$'))
#if($input.json('$.type').length())
#set($type=$inputRoot.type)
#else
#set($type="qna")
#end
{
"endpoint":"${ESVar.ESAddress}",
"method":"PUT",
"path":"/${Var.QnaIndex}/_doc/$input.params('ID')?refresh=wait_for",
"body":{
#foreach($paramName in $inputRoot.keySet())
#if( $paramName == 'q' && $type=="qna")
## generate quniqueterms field by concatenating questions in q array
"quniqueterms":" #foreach( $q in $inputRoot.get($paramName))$q #end ",
## replace q array with nested questions array
"questions":[
#foreach( $q in $inputRoot.get($paramName))
{"q":"$q"}
#if($foreach.hasNext),#end
#end
]
#if($foreach.hasNext),#end
#else
#set( $body = '$.'+$paramName)
"$paramName" :$input.json($body)
#if($foreach.hasNext),#end
#end
#end
}
}
================================================
FILE: source/templates/master/routes/qa/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const run = require('../util/temp-test').run;
const input = require('../util/temp-test').input;
module.exports={
single:{
head:{
send:test=>run(__dirname+'/'+'single/head',static(),test),
resp:test=>run(__dirname+'/'+'single/head.resp',static(),test)
},
delete:{
send:test=>run(__dirname+'/'+'single/delete',static(),test),
resp:function(test){
const body={
'_shards':{
successful:2
},
'_id':2,
result:'delete'
}
run(__dirname+'/'+'single/delete.resp',input(body),test)
}
},
put:{
send:function(test){
const body={
qid:'adad',
q:['b','c'],
card:{
title:''
}
}
run(__dirname+'/'+'single/put',input(body),test)
},
resp:function(test){
const body={
'_shards':{
successful:2
},
'_id':2,
result:'created'
}
run(__dirname+'/'+'single/put.resp',input(body),test)
}
}
},
collection:{
options:{
send:function(test){
run(__dirname+'/'+'single/options',input({}),test)
}
},
delete:{
sendQuery:function(test){
const body={query:'.*'}
run(__dirname+'/'+'collection/delete',input(body),test)
},
sendList:function(test){
const body={list:['ad','Ad']}
run(__dirname+'/'+'collection/delete',input(body),test)
},
resp:test=>run(__dirname+'/'+'collection/delete.resp',input({
deleted:100
}),test),
},
get:test=>run(__dirname+'/'+'single/get',{
input:{
body:'{}',
params:name=>{return {
from:'',
filter:'',
query:'',
perpage:'0'
}[name]}
}
},test),
list:test=>run(__dirname+'/'+'single/get',{
input:{
body:'{}',
params:name=>{return {
from:'',
filter:'filter',
query:'',
perpage:''
}[name]}
}
},test),
search:test=>run(__dirname+'/'+'single/get',{
input:{
body:'{}',
params:name=>{return {
from:'',
filter:'',
query:'search',
perpage:'',
topic:''
}[name] }
}
},test),
resp:function(test){
const body={
hits:{
total:10,
hits:[{
_score:10,
_id:'1',
_source:{
questions:[{q:'1'},{q:'2'}],
qid:'',
card:{
a:'1'
}
}
},{
_score:9,
_id:'2',
_source:{
questions:[{q:'1'},{q:'2'}],
qid:'',
card:{
a:'1'
}
}
}]
}
}
run(__dirname+'/'+'single/get.resp',input(body),test)
},
import:test=>run(__dirname+'/'+'single/put',{
input:{
body:'{}',
json:()=>'{}',
params:()=>'notall'
}
},test)
}
}
function static(){
return {
input:{
params:()=>'id'
}
}
}
================================================
FILE: source/templates/master/routes/root/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mock = require('../util/mock');
module.exports = {
rootGet: mock({
auth: 'NONE',
method: 'GET',
subTemplate: 'root/info',
resource: { 'Fn::GetAtt': ['API', 'RootResourceId'] },
}),
};
================================================
FILE: source/templates/master/routes/root/info.vm
================================================
#set ($root="https://${!context.domainName}/${!context.stage}")
{
"region":"${!stageVariables.Region}",
"Version":"${InfoVar.Version}",
"BuildDate":"${InfoVar.BuildDateString}",
"BotName":"Use LexV2 bot",
"BotVersion":"$LATEST",
"v2BotId": "${LexV2Bot.botId}",
"v2BotAliasId": "${LexV2Bot.botAliasId}",
"v2BotLocaleId": "${LexV2BotLocaleIds}",
"PoolId":"${IdPool}",
"StackName":"${AWS::StackName}",
"ClientIdClient":"${ClientClient}",
"ClientIdDesigner":"${ClientDesigner}",
"UserPool":"${UserPool}",
"StreamingWebSocketEndpoint": "$stageVariables.StreamingWebSocketEndpoint",
"SolutionHelper": "${SolutionHelper}",
"SettingsTable": "${SettingsTable}",
"Id":"$stageVariables.Id",
"_links":{
"root":{
"href":"$root"
},
"questions":{
"href":"$root/questions"
},
"crawler":{
"href":"$root/crawler"
},
"crawlerV2":{
"href":"$root/kendranativecrawler"
},
"bot":{
"href":"$root/bot"
},
"jobs":{
"href":"$root/jobs"
},
"connect":{
"href":"$root/connect"
},
"genesys":{
"href":"$root/genesys"
},
"translate":{
"href":"$root/translate"
},
"examples":{
"href":"$root/examples/documents"
},
"DesignerLogin":{
"href":"$stageVariables.DesignerLoginUrl"
},
"ClientLogin":{
"href":"$stageVariables.ClientLoginUrl"
},
"CognitoEndpoint":{
"href":"$stageVariables.CognitoEndpoint"
},
"Services":{
"href":"$root/services"
},
"OpenSearchDashboards":{
"href":"https://${Urls.OpenSearchDashboards}"
}
}
}
================================================
FILE: source/templates/master/routes/root/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
module.exports = {
info: (test) => run(`${__dirname}/` + 'info', {}, test),
};
================================================
FILE: source/templates/master/routes/services/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const resource = require('../util/resource');
const mock = require('../util/mock');
module.exports = {
Services: resource('services'),
ServicesGet: mock({
auth: 'AWS_IAM',
method: 'GET',
subTemplate: 'services/info',
resource: { Ref: 'Services' },
}),
};
================================================
FILE: source/templates/master/routes/services/info.vm
================================================
{
"opensearch":{
"qid":"${ESQidLambda.Arn}",
"proxy":"${ESProxyLambda.Arn}"
}
}
================================================
FILE: source/templates/master/routes/services/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
process.argv.push('--debug');
const { run } = require('../util/temp-test');
module.exports = {
info: (test) => run(`${__dirname}/` + 'info', {}, test),
};
================================================
FILE: source/templates/master/routes/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
bot: require('./bot/test'),
error: require('./error/test'),
health: require('./health/test'),
qa: require('./qa/test'),
root: require('./root/test'),
examples: require('./examples'),
jobs: require('./jobs'),
services: require('./services'),
};
================================================
FILE: source/templates/master/routes/util/context.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
stageVariables: {
Region: 'us-east-1',
PoolId: 'Pool-2121',
ClientId: 'Client-adad',
UserPool: 'User-adada',
CognitoEndpoint: 'www.example.com',
ESQidLambda: 'lambda',
ApiUrl: 'url',
BotName: 'bot',
SlotType: 'slot',
Intent: 'intent',
LambdaArn: 'ar',
ESEndpoint: 'test',
ESIndex: 'index',
ESType: 'type',
ImportBucket: 'import',
Id: 'id',
},
util: {
parseJson: JSON.parse,
urlDecode: (x) => x,
},
context: {
apiId: 'id',
stage: 'prod',
},
};
================================================
FILE: source/templates/master/routes/util/lambda.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const clean = require('clean-deep');
const _ = require('lodash');
module.exports = function (params) {
return clean({
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: params.authorization || 'NONE',
AuthorizerId: params.authorizerId,
HttpMethod: params.method.toUpperCase(),
Integration: {
Type: 'AWS',
IntegrationHttpMethod: 'POST',
Uri: {
'Fn::Join': ['', [
'arn:aws:apigateway:',
{ Ref: 'AWS::Region' },
':lambda:path/2015-03-31/functions/',
params.lambda || {
'Fn::Join': [':', [
{ 'Fn::GetAtt': ['FulfillmentLambda', 'Arn'] },
'live',
]],
},
'/invocations',
]],
},
IntegrationResponses: _.concat({
StatusCode: params.defaultResponse || 200,
ResponseParameters: params.responseParameters,
ResponseTemplates: {
'application/json': { 'Fn::Sub': params.responseTemplate },
},
}, {
SelectionPattern: '.*[InternalServiceError].*',
StatusCode: 500,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
}, {
SelectionPattern: '.*[BadRequest].*',
StatusCode: 400,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
}, {
SelectionPattern: '.*[Conflict].*',
StatusCode: 409,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
}, {
SelectionPattern: '.*[NotFound].*',
StatusCode: 404,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
},
{
SelectionPattern: '.*Exception.*',
StatusCode: 405,
ResponseTemplates: {
'application/json': fs.readFileSync(`${__dirname}/../error/error.vm`, 'utf8'),
},
}),
RequestParameters: params.parameterNames,
RequestTemplates: {
'application/json': params.subTemplate
? { 'Fn::Sub': params.subTemplate }
: params.template,
},
},
RequestModels: params.models,
RequestParameters: params.parameterLocations,
ResourceId: params.resource,
MethodResponses: [
{
StatusCode: params.defaultResponse || 200,
ResponseParameters: { 'method.response.header.date': true, ..._.mapValues(params.responseParameters, (x) => false) },
},
{
StatusCode: 404,
},
{
StatusCode: 405,
},
{
StatusCode: 500,
},
],
RestApiId: { Ref: 'API' },
},
}, {
emptyStrings: false,
});
};
================================================
FILE: source/templates/master/routes/util/mock.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../../util');
module.exports = function (opts) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: opts.auth || 'AWS_IAM',
HttpMethod: opts.method,
Integration: {
Type: 'MOCK',
IntegrationResponses: [{
ResponseTemplates: {
'application/json': opts.subTemplate
? {
'Fn::Sub': fs.readFileSync(
`${__dirname}/../${opts.subTemplate}.vm`,
'utf8',
),
}
: fs.readFileSync(
`${__dirname}/../${opts.template}.vm`,
'utf8',
),
},
StatusCode: '200',
}],
RequestTemplates: {
'application/json': '{"statusCode": 200}',
},
},
ResourceId: opts.resource,
MethodResponses: [{ StatusCode: 200 }],
RestApiId: { Ref: 'API' },
},
Metadata: { cfn_nag: util.cfnNag(['W59']) },
};
};
================================================
FILE: source/templates/master/routes/util/options.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = function (resource) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: 'NONE',
HttpMethod: 'OPTIONS',
Integration: {
Type: 'MOCK',
IntegrationResponses: [{
ResponseParameters: {
'method.response.header.Access-Control-Allow-Headers': '\'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token\'',
'method.response.header.Access-Control-Allow-Methods': '\'GET,POST,PUT,OPTIONS\'',
'method.response.header.Access-Control-Allow-Origin': '\'*\'',
},
ResponseTemplates: {
'application/json': '',
},
StatusCode: '200',
}],
RequestTemplates: { 'application/json': '{"statusCode": 200}' },
},
ResourceId: resource,
MethodResponses: [
{
StatusCode: 200,
ResponseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Origin': true,
},
},
{
StatusCode: 400,
},
],
RestApiId: { Ref: 'API' },
},
};
};
================================================
FILE: source/templates/master/routes/util/redirect.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../../util');
module.exports = function (url, resource) {
return {
Type: 'AWS::ApiGateway::Method',
Properties: {
AuthorizationType: 'NONE',
HttpMethod: 'GET',
Integration: {
Type: 'MOCK',
IntegrationResponses: [{
ResponseParameters: {
'method.response.header.location': {
'Fn::Join': ['', [
'\'', url, '\'',
]],
},
},
StatusCode: '302',
}],
RequestTemplates: {
'application/json': '{"statusCode": 302}',
},
},
ResourceId: resource,
MethodResponses: [{
StatusCode: 302,
ResponseParameters: {
'method.response.header.location': true,
},
}],
RestApiId: { Ref: 'API' },
},
Metadata: { cfn_nag: util.cfnNag(['W59']) },
};
};
================================================
FILE: source/templates/master/routes/util/resource.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = function (path, parent = { 'Fn::GetAtt': ['API', 'RootResourceId'] }) {
return {
Type: 'AWS::ApiGateway::Resource',
Properties: {
ParentId: parent,
PathPart: path,
RestApiId: { Ref: 'API' },
},
};
};
================================================
FILE: source/templates/master/routes/util/temp-test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Velocity = require('velocity');
const { JSONPath } = require('jsonpath-plus');
exports.run = function (name, context, test) {
const temp = new Velocity.Engine({
template: `${name}.vm`,
debug: true,
});
const result = temp.render(Object.assign(require('./context.js'), context));
console.log(result);
try {
const json = JSON.parse(result);
test.ok(true);
test.done();
} catch (e) {
console.log(e);
test.ok(false);
test.done();
}
};
exports.input = function (body) {
return {
input: {
params: (x) => body[x],
path: (x) => JSONPath({ path: x, json: body })[0],
json: (x) => JSON.stringify(JSONPath({ path: x, json: body })[0]),
},
};
};
================================================
FILE: source/templates/master/s3-clean/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
S3ClearCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/s3-clean.zip' },
BuildDate: (new Date()).toISOString(),
},
},
S3CleanLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-S3CleanLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
S3Clean: {
Type: 'AWS::Lambda::Function',
Metadata: { guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC') },
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/s3-clean.zip' },
},
Environment: {
Variables: {
...util.getCommonEnvironmentVariables(),
},
},
Description: 'This function clears all S3 objects from the bucket of a given S3-based resource',
Handler: 'lambda_function.handler',
LoggingConfig: {
LogGroup: { Ref: 'S3CleanLambdaLogGroup' },
},
Role: {
'Fn::GetAtt': ['CFNLambdaRole', 'Arn'],
},
Runtime: process.env.npm_package_config_pythonRuntime,
VpcConfig: {
'Fn::If': [
'VPCEnabled',
{
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
},
{ Ref: 'AWS::NoValue' },
],
},
TracingConfig: {
'Fn::If': [
'XRAYEnabled',
{ Mode: 'Active' },
{ Ref: 'AWS::NoValue' },
],
},
Tags: [{
Key: 'Type',
Value: 'S3 Clean',
}],
Timeout: 300,
},
},
};
================================================
FILE: source/templates/master/s3.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../util');
module.exports = {
Bucket: {
Type: 'AWS::S3::Bucket',
Metadata: { guard: util.cfnGuard('S3_BUCKET_NO_PUBLIC_RW_ACL') },
DependsOn : ['MainAccessLogBucket', 'MainAccessLogsBucketPolicy'],
DeletionPolicy: 'Delete',
Properties: {
VersioningConfiguration: {
Status: 'Enabled',
},
WebsiteConfiguration: {
IndexDocument: 'index.html',
},
PublicAccessBlockConfiguration: {
BlockPublicAcls: true,
BlockPublicPolicy: true,
IgnorePublicAcls: true,
RestrictPublicBuckets: true,
},
LoggingConfiguration: {
DestinationBucketName: { Ref: 'MainAccessLogBucket' },
LogFilePrefix: {"Fn::Join": ["", [{Ref: 'MainAccessLogBucket'},"/S3Bucket/"]]},
},
BucketEncryption: {
ServerSideEncryptionConfiguration: [{
ServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
}],
},
},
},
HTTPSOnlyBucketPolicy: util.httpsOnlyBucketPolicy(),
Clean: {
Type: 'Custom::S3Clean',
DependsOn: ['CFNInvokePolicy', 'HTTPSOnlyBucketPolicy'],
Properties: {
ServiceToken: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
Bucket: { Ref: 'Bucket' },
},
},
Unzip: {
Type: 'Custom::S3Unzip',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
SrcBucket: { Ref: 'BootstrapBucket' },
Key: {
'Fn::Join': ['', [
{ Ref: 'BootstrapPrefix' },
'/website.zip',
]],
},
DstBucket: { Ref: 'Bucket' },
buildDate: new Date(),
},
DependsOn: 'Clean',
},
S3AccessRole: {
Type: 'AWS::IAM::Role',
Metadata: { guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK') },
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'apigateway.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [{
PolicyName: 'S3AccessPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: [
's3:GetObject',
],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${ImportBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ExportBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${TestAllBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${Bucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${AssetBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ContentDesignerOutputBucket}/*' },
],
}, {
Effect: 'Allow',
Action: [
's3:PutObject',
],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${ExportBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${TestAllBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ContentDesignerOutputBucket}/*' },
],
}, {
Effect: 'Allow',
Action: [
's3:DeleteObject',
],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${ImportBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ExportBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${TestAllBucket}/*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ContentDesignerOutputBucket}/*' },
],
},
],
},
}],
},
},
};
================================================
FILE: source/templates/master/schemaLambda.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../util');
module.exports = {
SchemaLambdaCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/schema.zip' },
BuildDate: (new Date()).toISOString(),
},
},
SchemaLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-SchemaLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
SchemaLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/schema.zip' },
S3ObjectVersion: { Ref: 'SchemaLambdaCodeVersion' },
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'SchemaLambdaLogGroup' },
},
MemorySize: '128',
Role: { 'Fn::GetAtt': ['SchemaLambdaRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Api',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
SchemaLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
],
ManagedPolicyArns: [
{ Ref: 'QueryPolicy' },
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
================================================
FILE: source/templates/master/settings.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const defaultSettings = {
ENABLE_DEBUG_RESPONSES: 'false', // Appends a debugging message in the aws-lex-web-ui client for each QnABot response.
ENABLE_DEBUG_LOGGING: 'false', // Controls verbosity of the QnABot Cloudwatch log. Set to true to see QnABot debug messages.
ES_USE_KEYWORD_FILTERS: '${ES_USE_KEYWORD_FILTERS}', // Determines whether to detect keywords from Comprehend when searching for answers. Defaults to TRUE when not using Embeddings, and FALSE if using Embeddings.
ES_EXPAND_CONTRACTIONS: '{"you\'re":"you are","I\'m":"I am","can\'t":"cannot"}', // Dictionary of contractions and their expansions. Used to replace contracted forms such as `I'm` to `I am` for improved matching.
ES_KEYWORD_SYNTAX_TYPES: 'NOUN,PROPN,VERB,INTJ', // Comprehend will return these parts of speech found by Amazon Comprehend
ES_SYNTAX_CONFIDENCE_LIMIT: 0.20, // Comprehend makes a best effort to determine the parts of speech in a sentence. The keywords will only be used if the confidence limit is greater than this amount
ES_MINIMUM_SHOULD_MATCH: '2<75%', // A query string that specifies a matching condition for OpenSearch, such as the minimum number of words to match and/or the percentage of words that must match. Refer to https://opensearch.org/docs/latest/query-dsl/minimum-should-match/ for more information
ES_NO_HITS_QUESTION: 'no_hits', // The QID of the question when no answers could be found for a user's question
ES_ERROR_QUESTION: 'error_msg', // The QID of the question when no answers could be found for a user's question due to an error
ES_USE_FUZZY_MATCH: 'false', // Enables or disabled fuzzy matching which tries to correct for any possible misspellings. Refer to https://opensearch.org/docs/latest/query-dsl/term/fuzzy/
ES_PHRASE_BOOST: 4, // If the user's question is a phrase match to a question in the knowledge then boost the score by this factor.
ES_SCORE_ANSWER_FIELD: 'false', // If no 'qna' answer meets the score threshold, then query the answer field of qna items
ES_SCORE_TEXT_ITEM_PASSAGES: 'true', // If no 'qna' answer meets the score threshold, then query the text field of 'text' items
ENABLE_SENTIMENT_SUPPORT: 'true', // Determines whether to use Comprehend for sentiment analysis. Refer to https://docs.aws.amazon.com/comprehend/latest/dg/how-sentiment.html
ENABLE_MULTI_LANGUAGE_SUPPORT: 'false', // Enables automatic translation of incoming message to QnABot native language
ENABLE_CUSTOM_TERMINOLOGY: 'false', // Enables the user of custom terminology preventing specific phrases from being translated for brand protection
MINIMUM_CONFIDENCE_SCORE: 0.6, // The minimum confidence before Amazon Comprehend will determine the user's language
ALT_SEARCH_KENDRA_FALLBACK_CONFIDENCE_SCORE: 'HIGH', // Minimum score of a Kendra result that can be returned to the user. Should be one of 'VERY_HIGH'|'HIGH'|'MEDIUM'|'LOW'
ALT_SEARCH_KENDRA_FAQ_CONFIDENCE_SCORE: 'HIGH', // Minimum score of a Kendra result that can be returned to the user. Should be one of 'VERY_HIGH'|'HIGH'|'MEDIUM'|'LOW'
ALT_SEARCH_KENDRA_S3_SIGNED_URLS: 'true', // If S3 document URL is in the search result, convert to signed URL. Please ensure IAM FulfillmentLambdaRole has access to S3 objects in Kendra index (default role grants access to buckets starting with name QNA or qna).
ALT_SEARCH_KENDRA_S3_SIGNED_URL_EXPIRE_SECS: 300, // Expiry time for signed URLs
ALT_SEARCH_KENDRA_MAX_DOCUMENT_COUNT: 2, // limit number of document search results returned by Kendra fallback
ALT_SEARCH_KENDRA_TOP_ANSWER_MESSAGE: 'Amazon Kendra suggested answer.', // Message displayed in the client when a Kendra result is returned
ALT_SEARCH_KENDRA_FAQ_MESSAGE: 'Answer from Amazon Kendra FAQ.', // Message displayed in the client when a Kendra FAQ result is returned
ALT_SEARCH_KENDRA_ANSWER_MESSAGE:
'While I did not find an exact answer, these search results from Amazon Kendra might be helpful.', // Message displayed when a search comes from Kendra
ALT_SEARCH_KENDRA_RESPONSE_TYPES: 'ANSWER,DOCUMENT,QUESTION_ANSWER', // Types of responses Kendra will return to the user
ALT_SEARCH_KENDRA_ABBREVIATE_MESSAGE_FOR_SSML: 'true', // Abbreviates the Kendra result for voice users
KENDRA_FAQ_CONFIG_MAX_RETRIES: 8, // User can override number of max retries in AWS SDK configurations
KENDRA_FAQ_CONFIG_RETRY_DELAY: 600, // User can override number of miliseconds delay between retries in AWS SDK configurations
KENDRA_FAQ_ES_FALLBACK: 'true', // Optional OpenSearch Fallback engine for if KendraFAQ fails
ENABLE_KENDRA_WEB_INDEXER: 'false', // Enables web crawler -- indexes pages specified by KENDRA_INDEXER_URLS
KENDRA_INDEXER_URLS: '', // comma separated list of urls for Kendra to crawler
KENDRA_INDEXER_CRAWL_DEPTH: 3, // The number of recursive links to open to index
KENDRA_INDEXER_CRAWL_MODE: 'SUBDOMAINS', // Should be one of 'HOST_ONLY'|'SUBDOMAINS'|'EVERYTHING'
KENDRA_INDEXER_SCHEDULE: 'rate(1 day)', // See https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html for valid expressions
KENDRA_INDEXED_DOCUMENTS_LANGUAGES: 'en', // Comma separated language list, Eg: "en,es,fr". Should be one of supported Kendra languages mentioned in https://docs.aws.amazon.com/kendra/latest/dg/in-adding-languages.html
ERRORMESSAGE: 'Unfortunately I encountered an error when searching for your answer. Please ask me again later.',
EMPTYMESSAGE: 'You stumped me! Sadly I do not know how to answer your question.',
DEFAULT_ALEXA_LAUNCH_MESSAGE: 'Hello, Please ask a question', // Default message returned to the user when the QnABot Alexa app is activated
DEFAULT_ALEXA_REPROMPT:
'Please either answer the question, ask another question or say Goodbye to end the conversation.', // Default reprompt message returned to the user when the QnABot Alexa app is activated
DEFAULT_ALEXA_STOP_MESSAGE: 'Goodbye', // Default utterance to end the Alexa conversation
SMS_HINT_REMINDER_ENABLE: 'true',
SMS_HINT_REMINDER: ' (Feedback? Reply THUMBS UP or THUMBS DOWN. Ask HELP ME at any time)',
SMS_HINT_REMINDER_INTERVAL_HRS: 24,
IDENTITY_PROVIDER_JWKS_URLS: [], // User can override this empty list to add trusted IdPs (eg from Lex-Web-UI)
ENFORCE_VERIFIED_IDENTITY: 'false', // set to true to make QnABot require verified identity from client
NO_VERIFIED_IDENTITY_QUESTION: 'no_verified_identity', // if user identity cannot be verified, replace question string with this.
ELICIT_RESPONSE_MAX_RETRIES: 3, // Number of times an elicitResponse LexBot can be called before giving up when the Bot returns Failed
ELICIT_RESPONSE_RETRY_MESSAGE: 'Please try again.', // Default retry message when working with LexBot
ELICIT_RESPONSE_BOT_FAILURE_MESSAGE: 'Your response was not understood. Please start again.', // Message used when maximum number of retries is exceeded
ELICIT_RESPONSE_DEFAULT_MSG: 'Ok. ', // Ok. with an intentional blank space after the period
CONNECT_IGNORE_WORDS: '', // Throw an error if the transcript provided by connect only contains the words in this list (case insensitive)
CONNECT_ENABLE_VOICE_RESPONSE_INTERRUPT: 'false', // Return bot response in session attribute to enable contact flow to use response as an interruptible prompt.
CONNECT_NEXT_PROMPT_VARNAME: 'connect_nextPrompt', // Name of session var to use for next prompt
ENABLE_REDACTING: 'false', // Enable the system to redact log output
REDACTING_REGEX: '\\b\\d{4}\\b(?![-])|\\b\\d{9}\\b|\\b\\d{3}-\\d{2}-\\d{4}\\b', // default regex to use for redacting - redacts 4 digit numbers not followed by a '-', 9 digit numbers (SSN with no '-'s), and Standard SSN format
ENABLE_REDACTING_WITH_COMPREHEND: 'false', // Enables redaction of PII using Comprehend
COMPREHEND_REDACTING_CONFIDENCE_SCORE: 0.99, // Only redact PII if the score is above the configured percentage
COMPREHEND_REDACTING_ENTITY_TYPES:
'ADDRESS,EMAIL,SSN,PHONE,PASSWORD,BANK_ACCOUNT_NUMBER,BANK_ROUTING,CREDIT_DEBIT_NUMBER', // See https://aws.amazon.com/blogs/machine-learning/detecting-and-redacting-pii-using-amazon-comprehend/ for valid types
PII_REJECTION_ENABLED: false, // Enables PII Rejection
PII_REJECTION_QUESTION: 'pii_rejection_question', // If PII is found, the user's request (question) will change to this phrase
PII_REJECTION_REGEX: '\\b\\d{4}\\b(?![-])|\\b\\d{9}\\b|\\b\\d{3}-\\d{2}-\\d{4}\\b', // Regex to use to find PII.,
PII_REJECTION_ENTITY_TYPES: 'ADDRESS,EMAIL,SSN,PHONE,PASSWORD,BANK_ACCOUNT_NUMBER,BANK_ROUTING,CREDIT_DEBIT_NUMBER', // See https://aws.amazon.com/blogs/machine-learning/detecting-and-redacting-pii-using-amazon-comprehend/ for valid types
PII_REJECTION_CONFIDENCE_SCORE: 0.99, // Score confidence for Comprehend to reject
DISABLE_CLOUDWATCH_LOGGING: 'false', // disable all logging in fulfillment es query handler lambda. does not disable logging from Lambda Hooks or Conditional Chaining Lambda functions
MINIMAL_ES_LOGGING: 'false', // do not log utterances or session attributes to opensearch for OpenSearchDashboards logging
S3_PUT_REQUEST_ENCRYPTION: '', // enable header x-amz-server-side-encryption header and set with this value
BOT_ROUTER_WELCOME_BACK_MSG: 'Welcome back to QnABot.', // The text used by QnABot when ending communication from a specialty bot
BOT_ROUTER_EXIT_MSGS: 'exit,quit,goodbye,leave', // The exit phrases in comma separated list available for the a user to end communication with a specialty bot
RUN_LAMBDAHOOK_FROM_QUERY_STEP: 'true', // Enables the use of lambda hooks in the content designer question bank
LAMBDA_PREPROCESS_HOOK: '', // Lambda to invoke before matching a users query
LAMBDA_POSTPROCESS_HOOK: '', // Lambda to invoke after matching a users query
SEARCH_REPLACE_QUESTION_SUBSTRINGS: '', // Replaces substring in users utterance with replacement text before sending for matching
PROTECTED_UTTERANCES: 'help,help me,thumbs up,thumbs down,repeat,no_hits,no_verified_identity,reset language,detect language,english,french,spanish,german,italian,chinese,arabic,greek,repeat,can you repeat that,can you please say that again,please repeat that', // User utterances that will not be translated or disambiguated
EMBEDDINGS_ENABLE: '${EMBEDDINGS_ENABLE}', // Set to TRUE or FALSE to enable or disable use of embeddings for semantic search
EMBEDDINGS_SCORE_THRESHOLD: '${EMBEDDINGS_SCORE_THRESHOLD}', // If embedding similarity score is under threshold the match is rejected and QnABot reverts to scoring answer field (if ES_SCORE_ANSWER_FIELD is true).
EMBEDDINGS_SCORE_ANSWER_THRESHOLD: 0.8, // Applies only when if ES_SCORE_ANSWER_FIELD is true. If embedding similarity score on answer field is under threshold the match is rejected.
EMBEDDINGS_TEXT_PASSAGE_SCORE_THRESHOLD: '${EMBEDDINGS_TEXT_PASSAGE_SCORE_THRESHOLD}', // Applies only when if ES_SCORE_TEXT_ITEM_PASSAGES is true. If embedding similarity score on text item field is under threshold the match is rejected.
EMBEDDINGS_MAX_TOKEN_LIMIT: '${EMBEDDINGS_MAX_TOKEN_LIMIT}', // Max number of tokens the embeddings model can handle
LLM_GENERATE_QUERY_ENABLE: '${LLM_GENERATE_QUERY_ENABLE}', // Enables query disambiguation
LLM_GENERATE_QUERY_PROMPT_TEMPLATE: '${LLM_GENERATE_QUERY_PROMPT_TEMPLATE}', // Template to send to the LLM to generate a query from
LLM_GENERATE_QUERY_MODEL_PARAMS: '${LLM_GENERATE_QUERY_MODEL_PARAMS}', // LLM model parameters
LLM_QA_ENABLE: '${LLM_QA_ENABLE}', // Enables text summarization
LLM_QA_USE_KENDRA_RETRIEVAL_API: '${LLM_QA_ENABLE}', // Enables RAG with Kendra retrieval
LLM_QA_PROMPT_TEMPLATE: '${LLM_QA_PROMPT_TEMPLATE}', // Template to send to send to the LLM to summarize a response from
LLM_QA_MODEL_PARAMS: '${LLM_QA_MODEL_PARAMS}', // LLM model parameters
LLM_QA_PREFIX_MESSAGE: 'LLM Answer:', // Message to append in the chat client when the LLM generates a response
LLM_QA_SHOW_CONTEXT_TEXT: 'true', // Enables the full text passage to append to the chat client message that the LLM response was generated from.
LLM_QA_SHOW_SOURCE_LINKS: 'true', // Provides links to the sources the LLM generated text from
LLM_CHAT_HISTORY_MAX_MESSAGES: 12, // The maximum number of historical chat messages to send to the LLM for additional context. Used by both query generation and RAG.
LLM_QA_NO_HITS_REGEX: '${LLM_QA_NO_HITS_REGEX}', // Regex match of LLM response that will redirect to no hits
LLM_PROMPT_MAX_TOKEN_LIMIT: '${LLM_PROMPT_MAX_TOKEN_LIMIT}', // Max number of tokens the LLM can handle. QnABot will truncate the message to fit within this limit.
KNOWLEDGE_BASE_PREFIX_MESSAGE: 'From Knowledge Base:', // Message to append in the chat client when the knowledge base generates a response
KNOWLEDGE_BASE_SHOW_REFERENCES: 'true', // Enables the knowledge base to provide full-text references to the sources the knowledge base generated text from
KNOWLEDGE_BASE_S3_SIGNED_URLS: 'true', // Enables the knowledge base to provide signed URLs for the knowledge base documents.
KNOWLEDGE_BASE_S3_SIGNED_URL_EXPIRE_SECS: 300, // The number of seconds the signed URL will be valid for.
KNOWLEDGE_BASE_PROMPT_TEMPLATE: '${KNOWLEDGE_BASE_PROMPT_TEMPLATE}', // The template used to construct a prompt that is sent to the model for response generation.
KNOWLEDGE_BASE_MAX_NUMBER_OF_RETRIEVED_RESULTS: '', // Sets maximum number of retrieved result where each result corresponds to a source chunk. When querying a knowledge base, Amazon Bedrock returns up to five results by default.
KNOWLEDGE_BASE_SEARCH_TYPE: 'DEFAULT', // Select the search type which defines how data sources in the knowledge base are queried. If using an Amazon OpenSearch Serverless vector store that contains a filterable text field, you can specify whether to query the knowledge base with a HYBRID search using both vector embeddings and raw text, or SEMANTIC search using only vector embeddings. For other vector store configurations, only SEMANTIC search is available.
KNOWLEDGE_BASE_METADATA_FILTERS: '{}', // Specifies the filters to use on the metadata in the knowledge base data sources before returning results.
KNOWLEDGE_BASE_MODEL_PARAMS: '{}', // Customize the knowledge base model by providing inference parameters
BEDROCK_GUARDRAIL_IDENTIFIER: '', // A unique identifier for the guardrail that provides additional safeguards on top of the native protections of foundational models specified through cloudformation parameters LLMBedrockModelId and BedrockKnowledgeBaseModel
BEDROCK_GUARDRAIL_VERSION: '', // A version of the guardrail which takes effect only when specifying BEDROCK_GUARDRAIL_IDENTIFIER
};
const privateSettings = {
NATIVE_LANGUAGE: '${Language}', // Native Language is the Language chosen during a deployment which will be the core language used for the OpenSearch analyzers. NOTE: This language should only be changed from the Stack
EMBEDDINGS_MODEL_ID: '${EMBEDDINGS_MODEL_ID}', // Required when EmbeddingsApi is set to BEDROCK.
LLM_API: '${LLMApi}',
LLM_MODEL_ID: '${LLM_MODEL_ID}', // Required when LLMApi is set to BEDROCK.
KNOWLEDGE_BASE_ID: '${KNOWLEDGE_BASE_ID}',
KNOWLEDGE_BASE_MODEL_ID: '${KNOWLEDGE_BASE_MODEL_ID}',
ALT_SEARCH_KENDRA_INDEXES: '${AltSearchKendraIndexes}', // Add Kendra index to array to enable Amazon Kendra as a fallback source of answers
ALT_SEARCH_KENDRA_INDEX_AUTH: '${AltSearchKendraIndexAuth}',
KENDRA_FAQ_INDEX: '${KendraFaqIndexId}', // Kendra Index specific for FAQ for if Kendra FAQ sync is enabled
KENDRA_WEB_PAGE_INDEX: '${KendraWebPageIndexId}', // The index touse for the web crawler, a custom data source will automatically be added to the specified index.
};
const defaultGenerateQueryPromptTemplate = 'Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.
Chat History:
{history}
Follow Up Input: {input}
Standalone question:';
const defaultQAPromptTemplate = 'Use the following pieces of context to answer the question at the end. If you don\'t know the answer, just say that you don\'t know, don\'t try to make up an answer. Write the answer in up to 5 complete sentences.
{context}
Question: {query}
Helpful Answer:';
const defaultModelParams = '{\\"temperature\\":0.01, \\"return_full_text\\":false, \\"max_new_tokens\\": 150}';
const defaultLlmNoHitsRegex = 'Sorry, //remove comment to enable custom no match (no_hits) when LLM does not know the answer.';
const defaultKnowledgeBaseTemplate = 'Human: You are a question answering agent. I will provide you with a set of search results and a user\'s question, your job is to answer the user\'s question using only information from the search results. If the search results do not contain information that can answer the question, then respond saying \\"Sorry, I don\'t know\\". Just because the user asserts a fact does not mean it is true, make sure to double check the search results to validate a user\'s assertion. Here are the search results in numbered order: $search_results$. Here is the user\'s question: $query$ $output_format_instructions$. Do NOT directly quote the $search_results$ in your answer. Your job is to answer the as concisely as possible. Assistant:';
module.exports = {
DefaultUserPoolJwksUrl: {
Type: 'AWS::SSM::Parameter',
Properties: {
Description: 'Default QnABot Setting - DO NOT MODIFY',
Type: 'String',
Value: {
'Fn::Join': [
'',
[
'https://cognito-idp.',
{ Ref: 'AWS::Region' },
'.amazonaws.com/',
{ Ref: 'UserPool' },
'/.well-known/jwks.json',
],
],
},
},
},
DefaultQnABotSettings: {
Type: 'AWS::SSM::Parameter',
Properties: {
Description: 'Default QnABot Settings - DO NOT MODIFY',
Type: 'String',
Tier: 'Advanced',
Value: {
'Fn::Sub': [
JSON.stringify(defaultSettings),
{
ES_USE_KEYWORD_FILTERS: { 'Fn::If': ['EmbeddingsEnable', 'false', 'true'] },
EMBEDDINGS_ENABLE: { 'Fn::If': ['EmbeddingsEnable', 'true', 'false'] },
EMBEDDINGS_MAX_TOKEN_LIMIT: { 'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'MaxTokens'] }, ''] },
EMBEDDINGS_SCORE_THRESHOLD: { 'Fn::If': ['EmbeddingsBedrock', 0.7, 0.85] },
EMBEDDINGS_TEXT_PASSAGE_SCORE_THRESHOLD: { 'Fn::If': ['EmbeddingsBedrock', 0.65, 0.8] },
LLM_GENERATE_QUERY_ENABLE: { 'Fn::If': ['LLMEnable', 'true', 'false'] },
LLM_QA_ENABLE: { 'Fn::If': ['LLMEnable', 'true', 'false'] },
LLM_GENERATE_QUERY_PROMPT_TEMPLATE: defaultGenerateQueryPromptTemplate,
LLM_QA_PROMPT_TEMPLATE: defaultQAPromptTemplate,
LLM_GENERATE_QUERY_MODEL_PARAMS: '{}',
LLM_QA_MODEL_PARAMS: '{}',
LLM_PROMPT_MAX_TOKEN_LIMIT: 100000,
LLM_QA_NO_HITS_REGEX: defaultLlmNoHitsRegex,
KNOWLEDGE_BASE_PROMPT_TEMPLATE: defaultKnowledgeBaseTemplate,
},
],
},
},
},
PrivateQnABotSettings: {
Type: 'AWS::SSM::Parameter',
Properties: {
Description: 'Private QnABot Settings - DO NOT MODIFY',
Type: 'String',
Tier: 'Advanced',
Value: {
'Fn::Sub': [
JSON.stringify(privateSettings),
{
EMBEDDINGS_MODEL_ID: { 'Fn::If': ['EmbeddingsBedrock', { 'Fn::FindInMap': ['BedrockDefaults', {'Ref' : 'EmbeddingsBedrockModelId'}, 'ModelID'] }, ''] },
LLM_MODEL_ID: { 'Fn::If': ['LLMBedrock', {'Ref' : 'LLMBedrockModelId'}, ''] },
KNOWLEDGE_BASE_MODEL_ID: { 'Fn::If': ['BedrockKnowledgeBaseEnable', {'Ref' : 'BedrockKnowledgeBaseModel'}, ''] },
KNOWLEDGE_BASE_ID: { 'Fn::If': ['BedrockKnowledgeBaseEnable', {'Ref' : 'BedrockKnowledgeBaseId'}, ''] },
},
],
},
},
},
CustomQnABotSettings: {
Type: 'AWS::SSM::Parameter',
Properties: {
Description: 'Custom QnABot Settings - Modify to override defaults, or to add new settings',
Type: 'String',
Tier: 'Advanced',
Value: '{}',
},
},
SolutionHelperParameter: {
Type: 'AWS::SSM::Parameter',
Properties: {
Description: 'Solution Helper Parameter - DO NOT MODIFY',
Type: 'String',
Value: '{}',
},
},
};
================================================
FILE: source/templates/master/signup/README.md
================================================
# Cognito Lambda hooks
lambda used for cognito user registration/signup validation
================================================
FILE: source/templates/master/signup/__tests__/message.fixtures.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.event = {
request: {
userAttributes: {
email: 'XXXXXXXXXXXXXXXXX@amazon.com',
},
codeParameter: 'test',
usernameParameter: 'admin'
},
response: {},
};
================================================
FILE: source/templates/master/signup/__tests__/message.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { handler } = require('../message');
const { event } = require('./message.fixtures');
describe('message handler', () => {
const OLD_ENV = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
});
it('should return an error if the email domain is not approved', async () => {
process.env.APPROVED_DOMAIN = 'notamazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
await expect(handler(clonedEvent, context)).rejects.toThrow('EMAIL_DOMAIN_DENIED_ERR');
});
it('closes context if approved domain is not set', async () => {
process.env.APPROVED_DOMAIN = '';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
expect(clonedEvent.response.emailSubject).toEqual('QnABot Signup Verification Code');
expect(clonedEvent.response.emailMessage).toEqual('Hello, Your QnABot verification code is: test');
});
it('closes context if email matches domain and is verified', async () => {
process.env.APPROVED_DOMAIN = 'amazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
expect(clonedEvent.response.emailSubject).toEqual('QnABot Signup Verification Code');
expect(clonedEvent.response.emailMessage).toEqual('Hello, Your QnABot verification code is: test');
});
it('closes context if email matches domain and is not verified', async () => {
process.env.APPROVED_DOMAIN = 'amazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
clonedEvent.request.userAttributes.email_verified = 'False';
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
expect(clonedEvent.response.autoVerifyUser).toEqual(undefined);
});
afterAll(() => {
process.env = OLD_ENV;
});
});
================================================
FILE: source/templates/master/signup/__tests__/signup.fixtures.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.event = {
request: {
userAttributes: {
email: 'XXXXXXXXXXXXXXXXX@amazon.com',
email_verified: 'True',
},
},
response: {},
};
================================================
FILE: source/templates/master/signup/__tests__/signup.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { handler } = require('../signup');
const { event } = require('./signup.fixtures');
describe('signup handler', () => {
const OLD_ENV = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
});
it('should return an error if the email domain is not approved', async () => {
process.env.APPROVED_DOMAIN = 'notamazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
await expect(handler(clonedEvent, context)).rejects.toThrow('EMAIL_DOMAIN_DENIED_ERR');
});
it('closes context if approved domain is not set', async () => {
process.env.APPROVED_DOMAIN = '';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
});
it('closes context if email matches domain and is verified', async () => {
process.env.APPROVED_DOMAIN = 'amazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
expect(clonedEvent.response.autoVerifyEmail).toEqual(true);
expect(clonedEvent.response.autoConfirmUser).toEqual(true);
});
it('closes context if email matches domain and is not verified', async () => {
process.env.APPROVED_DOMAIN = 'amazon.com';
const clonedEvent = JSON.parse(JSON.stringify(event));
clonedEvent.request.userAttributes.email_verified = 'False';
const context = {};
const result = await handler(clonedEvent, context);
expect(result).toEqual(clonedEvent);
expect(clonedEvent.response.autoVerifyUser).toEqual(undefined);
});
afterAll(() => {
process.env = OLD_ENV;
});
});
================================================
FILE: source/templates/master/signup/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const util = require('../../util');
module.exports = {
SignupPermision: {
Type: 'AWS::Lambda::Permission',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: { 'Fn::GetAtt': ['SignupLambda', 'Arn'] },
Principal: 'cognito-idp.amazonaws.com',
SourceArn: { 'Fn::GetAtt': ['UserPool', 'Arn'] },
},
},
MessagePermision: {
Type: 'AWS::Lambda::Permission',
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: { 'Fn::GetAtt': ['MessageLambda', 'Arn'] },
Principal: 'cognito-idp.amazonaws.com',
SourceArn: { 'Fn::GetAtt': ['UserPool', 'Arn'] },
},
},
MessageLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-MessageLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
MessageLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/message.js`, 'utf8'),
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'MessageLambdaLogGroup' },
},
MemorySize: '128',
Environment: {
Variables: {
APPROVED_DOMAIN: {
'Fn::If': [
'Domain',
{ Ref: 'ApprovedDomain' },
{ Ref: 'AWS::NoValue' },
],
},
},
},
Role: {
'Fn::GetAtt': [
'SignupLambdaRole',
'Arn',
],
},
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Cognito',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
SignupLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-SignupLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
SignupLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
ZipFile: fs.readFileSync(`${__dirname}/signup.js`, 'utf8'),
},
Handler: 'index.handler',
LoggingConfig: {
LogGroup: { Ref: 'SignupLambdaLogGroup' },
},
MemorySize: '128',
Environment: {
Variables: {
APPROVED_DOMAIN: {
'Fn::If': [
'Domain',
{ Ref: 'ApprovedDomain' },
{ Ref: 'AWS::NoValue' },
],
},
},
},
Role: {
'Fn::GetAtt': [
'SignupLambdaRole',
'Arn',
],
},
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': ['VPCEnabled', {
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
}, { Ref: 'AWS::NoValue' }],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' },
{ Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
],
Tags: [{
Key: 'Type',
Value: 'Cognito',
}],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
SignupLambdaRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
};
================================================
FILE: source/templates/master/signup/message.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const subject = 'QnABot Signup Verification Code';
function message(code) {
return `Hello, Your QnABot verification code is: ${code}`;
}
function isEmailApproved(email, approvedDomain) {
if (!approvedDomain) return true;
// Escape special regex characters in the domain
const escapedDomain = approvedDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`^[A-Za-z0-9._%+-]+@${escapedDomain}$`);
return email.match(regex);
}
function setEmailResponse(event) {
event.response.emailSubject = subject;
event.response.emailMessage = message(event.request.codeParameter);
}
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
// Ensure response object exists
if (!event.response) {
event.response = {};
}
const approvedDomain = process.env.APPROVED_DOMAIN;
const email = event.request.userAttributes.email;
if (!isEmailApproved(email, approvedDomain)) {
// Throw error to reject user signup
throw new Error('EMAIL_DOMAIN_DENIED_ERR');
}
setEmailResponse(event);
// Return the event object for Cognito
return event;
} catch (error) {
console.log('Error in handler:', error);
// Re-throw to let Cognito handle the rejection
throw error;
}
};
================================================
FILE: source/templates/master/signup/signup.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
function isEmailApproved(email, approvedDomain) {
if (!approvedDomain) return true;
// Escape special regex characters in the domain
const escapedDomain = approvedDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`^[A-Za-z0-9._%+-]+@${escapedDomain}$`);
return email.match(regex);
}
function handleAutoVerify(event) {
if (event.request.userAttributes.email_verified === 'True') {
event.response.autoVerifyEmail = true;
event.response.autoConfirmUser = true;
}
}
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
try {
// Ensure response object exists
if (!event.response) {
event.response = {};
}
const approvedDomain = process.env.APPROVED_DOMAIN;
const email = event.request.userAttributes.email;
if (!isEmailApproved(email, approvedDomain)) {
// Throw error to reject user signup
throw new Error('EMAIL_DOMAIN_DENIED_ERR');
}
handleAutoVerify(event);
console.log('Returning event:', JSON.stringify(event, null, 2));
// Return the event object for Cognito
return event;
} catch (error) {
console.log('Error in handler:', error);
// Re-throw to let Cognito handle the rejection
throw error;
}
};
================================================
FILE: source/templates/master/solution-helper/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../util');
module.exports = {
SolutionHelperRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com'],
},
Action: ['sts:AssumeRole'],
},
],
},
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'SettingsTableReadAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'dynamodb:Scan',
],
Resource: [{ 'Fn::Sub': "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SettingsTable}" }],
},
],
},
},
{
PolicyName: 'GetParameterPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['ssm:GetParameter'],
Resource: [
{
'Fn::Join': [
'', [
'arn:',
{ 'Fn::Sub': '${AWS::Partition}:' },
'ssm:',
{ 'Fn::Sub': '${AWS::Region}:' },
{ 'Fn::Sub': '${AWS::AccountId}:' },
'parameter/',
{ Ref: 'SolutionHelperParameter' },
],
],
},
],
}],
},
},
{
PolicyName: 'PutParameterPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['ssm:PutParameter'],
Resource: [
{
'Fn::Join': [
'', [
'arn:',
{ 'Fn::Sub': '${AWS::Partition}:' },
'ssm:',
{ 'Fn::Sub': '${AWS::Region}:' },
{ 'Fn::Sub': '${AWS::AccountId}:' },
'parameter/',
{ Ref: 'SolutionHelperParameter' },
],
],
},
],
}],
},
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
SolutionHelperCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/solution-helper.zip' },
BuildDate: (new Date()).toISOString(),
},
},
SolutionHelperLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-SolutionHelper' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
SolutionHelper: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/solution-helper.zip' },
S3ObjectVersion: { Ref: 'SolutionHelperCodeVersion' },
},
Description: 'This function generates UUID for each deployment and sends anonymized data to the AWS Solutions team',
Handler: 'lambda_function.handler',
LoggingConfig: {
LogGroup: { Ref: 'SolutionHelperLogGroup' },
},
Role: {
'Fn::GetAtt': ['SolutionHelperRole', 'Arn'],
},
Environment: {
Variables: {
SOLUTION_PARAMETER: { Ref: 'SolutionHelperParameter' },
SETTINGS_TABLE: { Ref: 'SettingsTable' },
SOLUTION_ID : util.getCommonEnvironmentVariables().SOLUTION_ID,
},
},
Runtime: process.env.npm_package_config_pythonRuntime,
Timeout: 300,
VpcConfig: {
'Fn::If': [
'VPCEnabled',
{
SubnetIds: { Ref: 'VPCSubnetIdList' },
SecurityGroupIds: { Ref: 'VPCSecurityGroupIdList' },
},
{ Ref: 'AWS::NoValue' },
],
},
TracingConfig: {
'Fn::If': [
'XRAYEnabled',
{ Mode: 'Active' },
{ Ref: 'AWS::NoValue' },
],
},
Tags: [{
Key: 'Type',
Value: 'Solution Helper',
}],
},
DependsOn: [
'SolutionHelperRole',
],
Metadata: {
cfn_nag: util.cfnNag(['W89', 'W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
SolutionHelperCreateUniqueID: {
Type: 'Custom::CreateUUID',
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'SolutionHelper',
'Arn',
],
},
Resource: 'UUID',
},
UpdateReplacePolicy: 'Delete',
DeletionPolicy: 'Delete',
Condition: 'SolutionHelperSendAnonymizedDataToAWS',
},
SolutionHelperSendAnonymizedData: {
Type: 'Custom::AnonymizedData',
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'SolutionHelper',
'Arn',
],
},
Resource: 'AnonymizedMetric',
UUID: {
'Fn::GetAtt': [
'SolutionHelperCreateUniqueID',
'UUID',
],
},
Region: { Ref: 'AWS::Region' },
SolutionId: util.getCommonEnvironmentVariables().SOLUTION_ID,
Version: util.getCommonEnvironmentVariables().SOLUTION_VERSION,
OpenSearchNodeInstanceType: {
'Fn::If': [
'CreateDomain',
{ Ref: 'OpenSearchNodeInstanceType' },
{ Ref: 'AWS::NoValue' },
],
},
PublicOrPrivate: { Ref: 'PublicOrPrivate' },
Language: { Ref: 'Language' },
OpenSearchNodeCount: {
'Fn::If': [
'CreateDomain',
{ Ref: 'OpenSearchNodeCount' },
{ Ref: 'AWS::NoValue' },
],
},
OpenSearchEBSVolumeSize: {
'Fn::If': [
'CreateDomain',
{ Ref: 'OpenSearchEBSVolumeSize' },
{ Ref: 'AWS::NoValue' },
],
},
FulfillmentConcurrency: { Ref: 'FulfillmentConcurrency' },
InstallLexResponseBots: { Ref: 'InstallLexResponseBots' },
EmbeddingsApi: { Ref: 'EmbeddingsApi' },
EmbeddingsBedrockModelId: {
'Fn::If': [
'EmbeddingsBedrock',
{ Ref: 'EmbeddingsBedrockModelId' },
{ Ref: 'AWS::NoValue' },
],
},
LLMApi: { Ref: 'LLMApi' },
LLMBedrockModelId: {
'Fn::If': [
'LLMBedrock',
{ Ref: 'LLMBedrockModelId' },
{ Ref: 'AWS::NoValue' },
],
},
BedrockKnowledgeBaseModel: {
'Fn::If': [
'BedrockKnowledgeBaseEnable',
{ Ref: 'BedrockKnowledgeBaseModel' },
{ Ref: 'AWS::NoValue' },
],
},
KendraPluginsEnabled: {
'Fn::If': [
'KendraPluginsEnabled',
'YES',
'NO',
],
},
OpenSearchFineGrainAccessControl: { Ref: 'OpenSearchFineGrainAccessControl'},
EnableStreaming: { Ref: 'EnableStreaming' }
},
UpdateReplacePolicy: 'Delete',
DeletionPolicy: 'Delete',
Condition: 'SolutionHelperSendAnonymizedDataToAWS',
},
};
================================================
FILE: source/templates/master/streamingstack.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
StreamingStack: {
Type: 'AWS::CloudFormation::Stack',
Condition: 'StreamingEnabled',
Properties: {
TemplateURL: { 'Fn::Sub': 'https://${BootstrapBucket}.s3.${AWS::Region}.amazonaws.com/${BootstrapPrefix}/templates/streaming.json' },
Parameters: {
CFNLambda: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
CFNInvokePolicy: { Ref: 'CFNInvokePolicy' },
S3Clean: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
BootstrapBucket: { Ref: 'BootstrapBucket' },
BootstrapPrefix: { Ref: 'BootstrapPrefix' },
LogRetentionPeriod: { Ref: 'LogRetentionPeriod' },
XraySetting: { Ref: 'XraySetting' },
VPCSubnetIdList: { 'Fn::Join': [',', { Ref: 'VPCSubnetIdList' }] },
VPCSecurityGroupIdList: { 'Fn::Join': [',', { Ref: 'VPCSecurityGroupIdList' }] },
},
},
},
};
================================================
FILE: source/templates/master/tstallstack.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
TestAllStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: { 'Fn::Sub': 'https://${BootstrapBucket}.s3.${AWS::Region}.amazonaws.com/${BootstrapPrefix}/templates/testall.json' },
Parameters: {
CFNLambda: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
CFNInvokePolicy: { Ref: 'CFNInvokePolicy' },
S3Clean: { 'Fn::GetAtt': ['S3Clean', 'Arn'] },
LexV2BotId: { 'Fn::GetAtt': ['LexV2Bot', 'botId'] },
LexV2BotAliasId: { 'Fn::GetAtt': ['LexV2Bot', 'botAliasId'] },
BootstrapBucket: { Ref: 'BootstrapBucket' },
BootstrapPrefix: { Ref: 'BootstrapPrefix' },
VarIndex: { 'Fn::GetAtt': ['Var', 'QnaIndex'] },
EsEndpoint: { 'Fn::GetAtt': ['ESVar', 'ESAddress'] },
EsProxyLambda: { 'Fn::GetAtt': ['ESProxyLambda', 'Arn'] },
TestAllBucket: { Ref: 'TestAllBucket' },
ContentDesignerOutputBucket: { Ref: 'ContentDesignerOutputBucket' },
VPCSubnetIdList: { 'Fn::Join': [',', { Ref: 'VPCSubnetIdList' }] },
VPCSecurityGroupIdList: { 'Fn::Join': [',', { Ref: 'VPCSecurityGroupIdList' }] },
XraySetting: { Ref: 'XraySetting' },
AwsSdkLayerLambdaLayer: { Ref: 'AwsSdkLayerLambdaLayer' },
CommonModulesLambdaLayer:{ Ref: 'CommonModulesLambdaLayer' },
LogRetentionPeriod: { Ref: 'LogRetentionPeriod' },
},
},
},
};
================================================
FILE: source/templates/master/var.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const date = new Date();
module.exports = {
Var: {
Type: 'Custom::Variable',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
index: {
value: { Ref: 'AWS::StackName' },
op: 'toLowerCase',
},
QnAType: 'qna',
QuizType: 'quiz',
QnaIndex: {
value: { 'Fn::Sub': '${AWS::StackName}' },
op: 'toLowerCase',
},
ResponseBotStackName: {
value: { 'Fn::Sub': '${AWS::StackName}-examples' },
op: 'toLowerCase',
},
MetricsIndex: {
value: { 'Fn::Sub': '${AWS::StackName}-metrics' },
op: 'toLowerCase',
},
FeedbackIndex: {
value: { 'Fn::Sub': '${AWS::StackName}-feedback' },
op: 'toLowerCase',
},
},
},
InfoVar: {
Type: 'Custom::Variable',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Version: require('../../package.json').version,
BuildDateString: `${date.toDateString()} ${date.toTimeString()}`,
BuildDate: date,
},
},
ESVar: {
Type: 'Custom::Variable',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
ESArn: {
'Fn::If': [
'CreateDomain',
{ 'Fn::GetAtt': ['OpensearchDomain', 'DomainArn'] },
{ 'Fn::GetAtt': ['ESInfo', 'Arn'] },
],
},
ESAddress: {
'Fn::If': [
'CreateDomain',
{ 'Fn::GetAtt': ['OpensearchDomain', 'DomainEndpoint'] },
{ 'Fn::GetAtt': ['ESInfo', 'Endpoint'] },
],
},
ESDomain: {
'Fn::If': [
'CreateDomain',
{ Ref: 'OpensearchDomain' },
{ Ref: 'OpenSearchName' },
],
},
},
},
ApiUrl: {
Type: 'Custom::Variable',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Name: {
'Fn::Join': ['', [
'https://',
{ Ref: 'API' },
'.execute-api.',
{ Ref: 'AWS::Region' },
'.amazonaws.com/prod',
]],
},
},
},
Urls: {
Type: 'Custom::Variable',
Properties: {
ServiceToken: { 'Fn::GetAtt': ['CFNLambda', 'Arn'] },
Designer: {
'Fn::Join': ['', [
{ 'Fn::GetAtt': ['ApiUrl', 'Name'] },
'/static/index.html',
]],
},
Client: {
'Fn::Join': ['', [
{ 'Fn::GetAtt': ['ApiUrl', 'Name'] },
'/static/client.html',
]],
},
OpenSearchDashboards: { 'Fn::Sub': '${ESVar.ESAddress}/_dashboards/app/dashboards#/list' },
},
},
};
================================================
FILE: source/templates/package.json
================================================
{
"name": "qnabot-on-aws-infrastructure",
"version": "7.3.8",
"description": "QnABot infrastructure",
"scripts": {
"clean": "rm -rf node_modules",
"test": "jest",
"test:update:snapshot": "jest -u"
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com/solutions"
},
"license": "Apache-2.0",
"devDependencies": {
"@aws-sdk/client-lambda": "^3.621.0",
"@aws-sdk/client-s3": "^3.621.0",
"aws-sdk-client-mock": "^4.1.0",
"aws-sdk-client-mock-jest": "^4.1.0",
"jest": "^29.7.0"
},
"overrides": {
"cross-spawn": "^7.0.6",
"fast-xml-parser": "^5.5.6",
"micromatch": "^4.0.8",
"sinon": "^21.0.1"
}
}
================================================
FILE: source/templates/public/Makefile
================================================
BUILD=../../bin/build.js
NAME=$(shell basename $(shell pwd))
DST=../../build/templates/$(NAME).json
$(DST):./* ../../config.json ../master
$(BUILD) --stack $(NAME) --verbose
================================================
FILE: source/templates/public/README.md
================================================
# QnABot Public Master Template
public master template for QnABot. it is the master template with some parameters filled in and some output removed
================================================
FILE: source/templates/public/__tests__/expectedResult.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
Conditions: {
AdminSignUp: {
'Fn::Equals': [true, true],
},
FGACEnabled: {
"Fn::Equals": [true, true]
},
BedrockEnable: {
'Fn::Or': [
{
'Fn::Equals': [
{
'Ref': 'LLMApi'
},
'BEDROCK',
],
},
{
'Fn::Equals': [
{
'Ref': 'EmbeddingsApi'
},
'BEDROCK',
],
},
],
},
BuildExamples: {
'Fn::Equals': [true, true],
},
CreateDomain: {
'Fn::Equals': [true, true],
},
Domain: {
'Fn::Equals': [true, false],
},
DontCreateDomain: {
'Fn::Equals': [true, false],
},
EmbeddingsEnable: {
'Fn::Not': [
{
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'DISABLED',
],
},
],
},
EmbeddingsLambda: {
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'LAMBDA',
],
},
EmbeddingsLambdaArn: {
'Fn::Not': [
{
'Fn::Equals': [
{
Ref: 'EmbeddingsLambdaArn',
},
'',
],
},
],
},
EmbeddingsBedrock: {
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'BEDROCK',
],
},
Public: {
'Fn::Equals': [
{
Ref: 'PublicOrPrivate',
},
'PUBLIC',
],
},
SingleNode: {
'Fn::Equals': [
{
Ref: 'OpenSearchNodeCount',
},
'1',
],
},
VPCEnabled: {
'Fn::Equals': [true, false],
},
},
Description: '(SO0189) QnABot with admin and client websites - Version vx.x.x',
Metadata: {
'AWS::CloudFormation::Interface': {
ParameterGroups: [
{
Label: {
default: 'Step 2A: Set Basic Chatbot Parameters (required)',
},
Parameters: [
'Email',
'Username',
'PublicOrPrivate',
'Language',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting',
],
},
{
Label: {
default: 'Step 2B: Enable LLM for Semantic Search with Embeddings (optional)',
},
Parameters: [
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
],
},
{
Label: {
default: 'Step 2C: Enable LLM Retrieval and generative text question answering to use with Fallback Option (optional)',
},
Parameters: [
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'EnableStreaming'
],
},
{
Label: {
default: 'Step 2D: Select Data Sources as Fallback Option (optional)',
},
Parameters: [
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
],
},
],
},
},
Outputs: {},
Parameters: {},
};
================================================
FILE: source/templates/public/__tests__/indexConfig.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mockConfig = require('./mockConfig.json');
const mockMaster = require('./mockMaster');
const expectedResult = require('./expectedResult');
function create(filename) {
const file = `../${filename}`;
return require(file);
}
describe('public template with config', () => {
beforeEach(() => {
jest.mock('../../master', () => mockMaster);
jest.mock('../../../config.json', () => mockConfig);
});
it('uses default params if config file is not set', async () => {
const templateFile = await create('index.js');
expect(templateFile).toEqual(expectedResult);
});
afterEach(() => {
jest.resetModules();
});
});
================================================
FILE: source/templates/public/__tests__/indexNoConfig.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const expectedResult = require('./expectedResult');
const mockMaster = require('./mockMaster');
function create(filename) {
const file = `../${filename}`;
return require(file);
}
describe('publictemplate with no config', () => {
beforeEach(() => {
jest.mock('../../master', () => mockMaster);
jest.mock('../../../config.json', () => '{}');
});
it('uses default params if config file is not set', async () => {
const templateFile = await create('index.js');
expect(templateFile).toEqual(expectedResult);
});
afterEach(() => {
jest.resetModules();
});
});
================================================
FILE: source/templates/public/__tests__/mockConfig.json
================================================
{
"region": "us-east-1",
"profile": "default",
"publicBucket": "some-bucket",
"publicPrefix": "qnabot/v1.0.0",
"devEmail": "",
"devPublicOrPrivate": "PRIVATE",
"namespace": "dev",
"LexBotVersion": "LexV2 Only",
"LexV2BotLocaleIds": "en_US,es_US,fr_CA",
"stackNamePrefix": "QNA",
"skipCheckTemplate": true,
"noStackOutput": true,
"multiBucketDeployment": false,
"buildType": "AWSSolutions",
"FulfillmentConcurrency": 1,
"EmbeddingsApi": "BEDROCK",
"InstallLexResponseBots": true,
"KendraWebPageIndexId": "",
"KendraFaqIndexId": "",
"AltSearchKendraIndexes": "",
"devOpenSearchNodeCount": 1,
"devOpenSearchMasterNodeCount": 3,
"LLMApi": "BEDROCK",
"OpenSearchFineGrainAccessControl": "FALSE"
}
================================================
FILE: source/templates/public/__tests__/mockMaster.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
Parameters: {
},
Conditions: {
},
};
================================================
FILE: source/templates/public/index.js
================================================
#! /usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const config = require('../../config.json');
module.exports = Promise.resolve(require('../master')).then((base) => {
// customize description
base.Description = `(SO0189) QnABot with admin and client websites - Version v${process.env.npm_package_version}`;
base.Outputs = _.pick(base.Outputs, [
'ContentDesignerURL',
'ClientURL',
'CloudWatchDashboardURL',
'UserPoolURL',
'LexV2BotName',
'LexV2BotId',
'LexV2BotAlias',
'LexV2BotAliasId',
'LexV2Intent',
'LexV2IntentFallback',
'LexV2BotLocaleIds',
'FeedbackSNSTopic',
'ESProxyLambda',
'OpenSearchDomainEndpoint',
'OpenSearchIndex',
'MetricsBucket',
'TestAllBucket',
'ContentDesignerOutputBucket',
'StreamingWebSocketEndpoint',
'SettingsTable'
]);
base.Parameters = _.pick(base.Parameters, [
'Email',
'Username',
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'AltSearchKendraIndexAuth',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'PublicOrPrivate',
'Language',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting',
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
'LogRetentionPeriod',
'EnableStreaming',
]);
base.Metadata = {
'AWS::CloudFormation::Interface': {
ParameterGroups: [
{
Label: {
default: 'Step 2A: Set Basic Chatbot Parameters (required)',
},
Parameters: [
'Email',
'Username',
'PublicOrPrivate',
'Language',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting',
],
},
{
Label: {
default: 'Step 2B: Enable LLM for Semantic Search with Embeddings (optional)',
},
Parameters: [
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
],
},
{
Label: {
default: 'Step 2C: Enable LLM Retrieval and generative text question answering to use with Fallback Option (optional)',
},
Parameters: [
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'EnableStreaming',
],
},
{
Label: {
default: 'Step 2D: Select Data Sources as Fallback Option (optional)',
},
Parameters: [
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
],
},
],
},
};
base.Conditions.Public = { 'Fn::Equals': [{ Ref: 'PublicOrPrivate' }, 'PUBLIC'] };
base.Conditions.AdminSignUp = { 'Fn::Equals': [true, true] };
base.Conditions.FGACEnabled = { 'Fn::Equals': [true, true ] };
base.Conditions.Domain = { 'Fn::Equals': [true, false] };
base.Conditions.BuildExamples = { 'Fn::Equals': [true, true] };
base.Conditions.CreateDomain = { 'Fn::Equals': [true, true] };
base.Conditions.DontCreateDomain = { 'Fn::Equals': [true, false] };
base.Conditions.VPCEnabled = { 'Fn::Equals': [true, false] };
base.Conditions.SingleNode = { 'Fn::Equals': [{ Ref: 'OpenSearchNodeCount' }, '1'] };
base.Conditions.BedrockEnable = { 'Fn::Or': [{ 'Fn::Equals': [{ Ref: 'LLMApi' }, 'BEDROCK'] }, { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] }] };
base.Conditions.EmbeddingsEnable = { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'DISABLED'] }] };
base.Conditions.EmbeddingsBedrock = { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] };
base.Conditions.EmbeddingsLambda = { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'LAMBDA'] };
base.Conditions.EmbeddingsLambdaArn = { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsLambdaArn' }, ''] }] };
let out = JSON.stringify(base);
if (config.buildType == 'AWSSolutions') {
out = out.replace(
/{"Ref":"BootstrapBucket"}/g,
`{"Fn::Sub": "${config.publicBucket}-\${AWS::Region}"}`,
);
out = out.replace(
/\${BootstrapBucket}/g,
`${config.publicBucket}-\${AWS::Region}`,
);
} else {
out = out.replace(
/{"Ref":"BootstrapBucket"}/g,
`"${config.publicBucket}"`,
);
out = out.replace(
/\${BootstrapBucket}/g,
`${config.publicBucket}`,
);
}
out = out.replace(
/{"Ref":"OpenSearchName"}/g,
'"EMPTY"',
);
out = out.replace(
/{"Ref":"ApprovedDomain"}/g,
'"EMPTY"',
);
out = out.replace(
/\${BootstrapPrefix}/g,
`${config.publicPrefix}`,
);
out = out.replace(
/{"Ref":"BootstrapPrefix"}/g,
`"${config.publicPrefix}"`,
);
out = out.replace(
/{"Ref":"VPCSubnetIdList"}/g,
'[{"Ref":"AWS::NoValue"}]',
);
out = out.replace(
/{"Ref":"VPCSecurityGroupIdList"}/g,
'[{"Ref":"AWS::NoValue"}]',
);
return JSON.parse(out);
});
================================================
FILE: source/templates/public-vpc-support/Makefile
================================================
BUILD=../../bin/build.js
NAME=$(shell basename $(shell pwd))
DST=../../build/templates/$(NAME).json
$(DST):./* ../../config.json ../master ../../build/templates/public.json
$(BUILD) --stack $(NAME) --verbose
================================================
FILE: source/templates/public-vpc-support/README.md
================================================
# QnABot Public Master Template with VPC support
public master template for QnABot. it is the master template with some parameters filled in and some output removed
================================================
FILE: source/templates/public-vpc-support/__tests__/expectedResult.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
Conditions: {
AdminSignUp: {
'Fn::Equals': [true, true],
},
BuildExamples: {
'Fn::Equals': [true, true],
},
FGACEnabled: {
'Fn::Equals': [true, true],
},
BedrockEnable: {
'Fn::Or': [
{
'Fn::Equals': [
{
'Ref': 'LLMApi'
},
'BEDROCK',
],
},
{
'Fn::Equals': [
{
'Ref': 'EmbeddingsApi'
},
'BEDROCK',
],
},
],
},
CreateDomain: {
'Fn::Equals': [true, true],
},
Domain: {
'Fn::Equals': [true, false],
},
DontCreateDomain: {
'Fn::Equals': [true, false],
},
EmbeddingsEnable: {
'Fn::Not': [
{
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'DISABLED',
],
},
],
},
EmbeddingsLambda: {
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'LAMBDA',
],
},
EmbeddingsLambdaArn: {
'Fn::Not': [
{
'Fn::Equals': [
{
Ref: 'EmbeddingsLambdaArn',
},
'',
],
},
],
},
EmbeddingsBedrock: {
'Fn::Equals': [
{
Ref: 'EmbeddingsApi',
},
'BEDROCK',
],
},
Public: {
'Fn::Equals': [
{
Ref: 'PublicOrPrivate',
},
'PUBLIC',
],
},
VPCEnabled: {
'Fn::Not': [
{
'Fn::Equals': [
'',
{
'Fn::Join': [
'',
{
Ref: 'VPCSecurityGroupIdList',
},
],
},
],
},
],
},
},
Description: '(SO0189) QnABot with admin and client websites - Version vx.x.x',
Metadata: {
'AWS::CloudFormation::Interface': {
ParameterGroups: [
{
Label: {
default: 'Step 2A: Set Basic Chatbot Parameters (required)',
},
Parameters: [
'Email',
'Username',
'PublicOrPrivate',
'Language',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting',
],
},
{
Label: {
default: 'Step 2B: Set VPC parameters to deploy QnABot in an existing VPC (required)',
},
Parameters: [
'VPCSubnetIdList',
'VPCSecurityGroupIdList',
],
},
{
Label: {
default: 'Step 2C: Enable LLM for Semantic Search with Embeddings (optional)',
},
Parameters: [
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
],
},
{
Label: {
default: 'Step 2D: Enable LLM Retrieval and generative text question answering to use with Fallback Option (optional)',
},
Parameters: [
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'EnableStreaming'
],
},
{
Label: {
default: 'Step 2E: Select Data Sources as Fallback Option (optional)',
},
Parameters: [
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
],
},
],
},
},
Outputs: {},
Parameters: {
VPCSubnetIdList: {
Type: 'List',
AllowedPattern: '.+',
},
VPCSecurityGroupIdList: {
Type: 'List',
AllowedPattern: '.+',
},
},
};
================================================
FILE: source/templates/public-vpc-support/__tests__/indexConfig.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mockConfig = require('./mockConfig.json');
const mockMaster = require('./mockMaster');
const expectedResult = require('./expectedResult');
function create(filename) {
const file = `../${filename}`;
return require(file);
}
describe('public vpc template with config', () => {
beforeEach(() => {
jest.mock('../../master', () => mockMaster);
jest.mock('../../../config.json', () => mockConfig);
});
it('uses default params if config file is not set', async () => {
const templateFile = await create('index.js');
expect(templateFile).toEqual(expectedResult);
});
afterEach(() => {
jest.resetModules();
});
});
================================================
FILE: source/templates/public-vpc-support/__tests__/indexNoConfig.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const expectedResult = require('./expectedResult');
const mockMaster = require('./mockMaster');
function create(filename) {
const file = `../${filename}`;
return require(file);
}
describe('public vpc template with no config', () => {
beforeEach(() => {
jest.mock('../../master', () => mockMaster);
jest.mock('../../../config.json', () => '{}');
});
it('uses default params if config file is not set', async () => {
const templateFile = await create('index.js');
expect(templateFile).toEqual(expectedResult);
});
afterEach(() => {
jest.resetModules();
});
});
================================================
FILE: source/templates/public-vpc-support/__tests__/mockConfig.json
================================================
{
"region": "us-east-1",
"profile": "default",
"publicBucket": "some-bucket",
"publicPrefix": "qnabot/v1.0.0",
"devEmail": "",
"devPublicOrPrivate": "PRIVATE",
"namespace": "dev",
"LexBotVersion": "LexV2 Only",
"LexV2BotLocaleIds": "en_US,es_US,fr_CA",
"stackNamePrefix": "QNA",
"skipCheckTemplate": true,
"noStackOutput": true,
"multiBucketDeployment": false,
"buildType": "AWSSolutions",
"FulfillmentConcurrency": 1,
"EmbeddingsApi": "BEDROCK",
"InstallLexResponseBots": true,
"DefaultKendraIndexId": "",
"devOpenSearchNodeCount": 1,
"devOpenSearchMasterNodeCount": 1,
"LLMApi": "BEDROCK",
"OpenSearchFineGrainAccessControl": "FALSE",
"EnableStreaming": "FALSE"
}
================================================
FILE: source/templates/public-vpc-support/__tests__/mockMaster.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
Parameters: {
VPCSubnetIdList: {},
VPCSecurityGroupIdList: {},
},
Conditions: {
},
};
================================================
FILE: source/templates/public-vpc-support/index.js
================================================
#! /usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const config = require('../../config.json');
module.exports = Promise.resolve(require('../master')).then((base) => {
// customize description
base.Description = `(SO0189) QnABot with admin and client websites - Version v${process.env.npm_package_version}`;
base.Outputs = _.pick(base.Outputs, [
'ContentDesignerURL',
'ClientURL',
'CloudWatchDashboardURL',
'UserPoolURL',
'LexV2BotName',
'LexV2BotId',
'LexV2BotAlias',
'LexV2BotAliasId',
'LexV2Intent',
'LexV2IntentFallback',
'LexV2BotLocaleIds',
'FeedbackSNSTopic',
'ESProxyLambda',
'OpenSearchDomainEndpoint',
'OpenSearchIndex',
'MetricsBucket',
'TestAllBucket',
'ContentDesignerOutputBucket',
'StreamingWebSocketEndpoint',
'SettingsTable'
]);
base.Parameters = _.pick(base.Parameters, [
'Email',
'Username',
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'AltSearchKendraIndexAuth',
'PublicOrPrivate',
'Language',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'VPCSubnetIdList',
'VPCSecurityGroupIdList',
'XraySetting',
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
'LogRetentionPeriod',
'EnableStreaming',
]);
base.Metadata = {
'AWS::CloudFormation::Interface': {
ParameterGroups: [
{
Label: {
default: 'Step 2A: Set Basic Chatbot Parameters (required)',
},
Parameters: [
'Email',
'Username',
'PublicOrPrivate',
'Language',
'OpenSearchDedicatedMasterNodes',
'OpenSearchMasterNodeInstanceType',
'OpenSearchMasterNodeCount',
'OpenSearchNodeInstanceType',
'OpenSearchNodeCount',
'OpenSearchEBSVolumeSize',
'OpenSearchDashboardsRetentionMinutes',
'OpenSearchFineGrainAccessControl',
'LexV2BotLocaleIds',
'LexBotVersion',
'InstallLexResponseBots',
'FulfillmentConcurrency',
'XraySetting'
],
},
{
Label: {
default: 'Step 2B: Set VPC parameters to deploy QnABot in an existing VPC (required)',
},
Parameters: [
'VPCSubnetIdList',
'VPCSecurityGroupIdList',
],
},
{
Label: {
default: 'Step 2C: Enable LLM for Semantic Search with Embeddings (optional)',
},
Parameters: [
'EmbeddingsApi',
'EmbeddingsBedrockModelId',
'EmbeddingsLambdaArn',
'EmbeddingsLambdaDimensions',
],
},
{
Label: {
default: 'Step 2D: Enable LLM Retrieval and generative text question answering to use with Fallback Option (optional)',
},
Parameters: [
'LLMApi',
'LLMBedrockModelId',
'LLMLambdaArn',
'EnableStreaming',
],
},
{
Label: {
default: 'Step 2E: Select Data Sources as Fallback Option (optional)',
},
Parameters: [
'KendraWebPageIndexId',
'KendraFaqIndexId',
'AltSearchKendraIndexes',
'BedrockKnowledgeBaseId',
'BedrockKnowledgeBaseModel',
],
},
],
},
};
base.Conditions.Public = { 'Fn::Equals': [{ Ref: 'PublicOrPrivate' }, 'PUBLIC'] };
base.Conditions.AdminSignUp = { 'Fn::Equals': [true, true] };
base.Conditions.Domain = { 'Fn::Equals': [true, false] };
base.Conditions.BuildExamples = { 'Fn::Equals': [true, true] };
base.Conditions.FGACEnabled = { 'Fn::Equals': [true, true] };
base.Conditions.CreateDomain = { 'Fn::Equals': [true, true] };
base.Conditions.DontCreateDomain = { 'Fn::Equals': [true, false] };
base.Conditions.VPCEnabled = {
'Fn::Not': [
{
'Fn::Equals': ['',
{ 'Fn::Join': ['', { Ref: 'VPCSecurityGroupIdList' }] },
],
},
],
};
base.Conditions.BedrockEnable = { 'Fn::Or': [{ 'Fn::Equals': [{ Ref: 'LLMApi' }, 'BEDROCK'] }, { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] }] };
base.Conditions.EmbeddingsEnable = { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'DISABLED'] }] };
base.Conditions.EmbeddingsBedrock = { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'BEDROCK'] };
base.Conditions.EmbeddingsLambda = { 'Fn::Equals': [{ Ref: 'EmbeddingsApi' }, 'LAMBDA'] };
base.Conditions.EmbeddingsLambdaArn = { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'EmbeddingsLambdaArn' }, ''] }] };
base.Parameters.VPCSubnetIdList = {
Type: 'List',
Description: base.Parameters.VPCSubnetIdList.Description,
AllowedPattern: '.+',
ConstraintDescription: base.Parameters.VPCSubnetIdList.ConstraintDescription,
};
base.Parameters.VPCSecurityGroupIdList = {
Type: 'List',
Description: base.Parameters.VPCSecurityGroupIdList.Description,
AllowedPattern: '.+',
ConstraintDescription: base.Parameters.VPCSecurityGroupIdList.ConstraintDescription,
};
let out = JSON.stringify(base);
if (config.buildType == 'AWSSolutions') {
out = out.replace(
/{"Ref":"BootstrapBucket"}/g,
`{"Fn::Sub": "${config.publicBucket}-\${AWS::Region}"}`,
);
out = out.replace(
/\${BootstrapBucket}/g,
`${config.publicBucket}-\${AWS::Region}`,
);
} else {
out = out.replace(
/{"Ref":"BootstrapBucket"}/g,
`"${config.publicBucket}"`,
);
out = out.replace(
/\${BootstrapBucket}/g,
`${config.publicBucket}`,
);
}
out = out.replace(
/{"Ref":"OpenSearchName"}/g,
'"EMPTY"',
);
out = out.replace(
/{"Ref":"ApprovedDomain"}/g,
'"EMPTY"',
);
out = out.replace(
/\${BootstrapPrefix}/g,
`${config.publicPrefix}`,
);
out = out.replace(
/{"Ref":"BootstrapPrefix"}/g,
`"${config.publicPrefix}"`,
);
return JSON.parse(out);
});
================================================
FILE: source/templates/streaming/Makefile
================================================
BUILD=../../bin/build.js
NAME=$(shell basename $(shell pwd))
DST=../../build/templates/$(NAME).json
default: streamingstack
streamingstack:
$(BUILD) --stack $(NAME) --verbose
================================================
FILE: source/templates/streaming/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const resources = require('./resources');
const outputs = require('./outputs');
module.exports = {
AWSTemplateFormatVersion: '2010-09-09',
Description: `(SO0189n-streaming) This template deploys resources to allow streaming responses - Version v${process.env.npm_package_version}`,
Parameters: {
CFNLambda: { Type: 'String' },
CFNInvokePolicy: { Type: 'String' },
S3Clean: { Type: 'String' },
BootstrapBucket: { Type: 'String' },
BootstrapPrefix: { Type: 'String' },
LogRetentionPeriod: { Type: 'Number' },
XraySetting: { Type: 'String' },
VPCSubnetIdList: { Type: 'String' },
VPCSecurityGroupIdList: { Type: 'String' },
},
Conditions: {
VPCEnabled: { 'Fn::Not': [{ 'Fn::Equals': ['', { Ref: 'VPCSecurityGroupIdList' }] }] },
XRAYEnabled: { 'Fn::Equals': [{ Ref: 'XraySetting' }, 'TRUE'] },
LogRetentionPeriodIsNotZero: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'LogRetentionPeriod' }, 0] }] }
},
Resources: _.merge({}, resources),
Outputs: _.merge({}, outputs)
};
================================================
FILE: source/templates/streaming/outputs.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
StreamingWebSocketApiId: {
Value: { Ref: 'WebSocketAPI' }
},
StreamingWebSocketEndpoint: {
Value: {
'Fn::Sub': 'wss://${WebSocketAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod'
}
},
StreamingLambdaArn: {
Value: { 'Fn::GetAtt': ['StreamingLambda', 'Arn'] }
},
StreamingDynamoDbTable: {
Value: { Ref: 'StreamingDynamoTable' }
},
StreamingDynamoDbTableArn: {
Value: { 'Fn::GetAtt': ['StreamingDynamoTable', 'Arn'] }
}
};
================================================
FILE: source/templates/streaming/resources.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/* eslint-disable quotes */
/* eslint-disable indent */
const util = require('../util');
module.exports = {
StreamingLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
VpcConfig: {
'Fn::If': [
'VPCEnabled',
{
Subnets: { 'Fn::Split': [',', { Ref: 'VPCSubnetIdList' }] },
SecurityGroupIds: { 'Fn::Split': [',', { Ref: 'VPCSecurityGroupIdList' }] }
},
{ Ref: 'AWS::NoValue' }
]
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' }, { Ref: 'AWS::NoValue' }]
},
Description: 'AWS Lambda Function to initiate web socket connection for streaming',
Handler: 'index.handler',
Role: { 'Fn::GetAtt': ['StreamingLambdaExecutionRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
MemorySize: 128,
Timeout: 900,
Environment: {
Variables: {
STREAMING_TABLE: { Ref: 'StreamingDynamoTable' },
...util.getCommonEnvironmentVariables()
}
},
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/streaming.zip' },
S3ObjectVersion: { Ref: 'StreamingCodeVersion' }
}
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC')
}
},
StreamingLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-StreamingLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] }
]
]
},
RetentionInDays: {
'Fn::If': ['LogRetentionPeriodIsNotZero', { Ref: 'LogRetentionPeriod' }, { Ref: 'AWS::NoValue' }]
}
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK')
}
},
StreamingLambdaExecutionRole: {
Type: 'AWS::IAM::Role',
Properties: {
ManagedPolicyArns: {
'Fn::If': [
'VPCEnabled',
['arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole'],
{ Ref: 'AWS::NoValue' }
]
},
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com']
},
Action: ['sts:AssumeRole']
}
]
},
Path: '/',
Policies: [
{
PolicyName: 'StreamingExecutionPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
}
]
},
{
Effect: 'Allow',
Action: ['dynamodb:PutItem'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${StreamingDynamoTable}'
}
]
}
]
}
}
]
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK')
}
},
StreamingCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { Ref: 'CFNLambda' },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/streaming.zip' },
BuildDate: new Date().toISOString()
}
},
StreamingLambdaInvokePermission: {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: { Ref: 'StreamingLambda' },
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
SourceArn: {
'Fn::Sub': 'arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketAPI}/*'
}
}
},
StreamingDynamoTable: {
Type: 'AWS::DynamoDB::Table',
Properties: {
AttributeDefinitions: [
{
AttributeName: 'sessionId',
AttributeType: 'S'
}
],
KeySchema: [
{
AttributeName: 'sessionId',
KeyType: 'HASH'
}
],
BillingMode: 'PAY_PER_REQUEST',
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true
},
SSESpecification: {
SSEEnabled: true
},
TimeToLiveSpecification: {
AttributeName: 'ttl',
Enabled: true
}
},
Metadata: { cfn_nag: util.cfnNag(['W74']) }
},
WebSocketAPI: {
Type: 'AWS::ApiGatewayV2::Api',
Properties: {
Name: 'QNA-WEBSocketAPI',
ProtocolType: 'WEBSOCKET',
RouteSelectionExpression: '$request.body.action'
}
},
ConnectRoute: {
Type: 'AWS::ApiGatewayV2::Route',
Properties: {
ApiId: { Ref: 'WebSocketAPI' },
RouteKey: '$connect',
AuthorizationType: 'AWS_IAM',
OperationName: 'ConnectRoute',
Target: {
'Fn::Join': ['/', ['integrations', { Ref: 'ConnectIntegration' }]]
}
}
},
ConnectIntegration: {
Type: 'AWS::ApiGatewayV2::Integration',
Properties: {
ApiId: { Ref: 'WebSocketAPI' },
Description: 'Connect Integration',
IntegrationType: 'AWS_PROXY',
IntegrationUri: {
'Fn::Sub':
'arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${StreamingLambda.Arn}/invocations'
}
}
},
PingRoute: {
Type: 'AWS::ApiGatewayV2::Route',
Properties: {
ApiId: { Ref: 'WebSocketAPI' },
RouteKey: 'ping',
OperationName: 'PingRoute',
Target: {
'Fn::Join': ['/', ['integrations', { Ref: 'PingIntegration' }]]
}
}
},
PingIntegration: {
Type: 'AWS::ApiGatewayV2::Integration',
Properties: {
ApiId: { Ref: 'WebSocketAPI' },
Description: 'Ping Integration',
IntegrationType: 'MOCK',
PassthroughBehavior: 'WHEN_NO_MATCH',
RequestTemplates: {
'application/json': '{"statusCode":200}'
}
}
},
WebSocketDeployment: {
Type: 'AWS::ApiGatewayV2::Deployment',
DependsOn: ['ConnectRoute', 'PingRoute'],
Properties: {
ApiId: { Ref: 'WebSocketAPI' }
}
},
WebSocketStage: {
Type: 'AWS::ApiGatewayV2::Stage',
Properties: {
StageName: 'Prod',
Description: 'QnABot WebSocket Stage',
DeploymentId: { Ref: 'WebSocketDeployment' },
ApiId: { Ref: 'WebSocketAPI' },
DefaultRouteSettings: {
LoggingLevel: 'INFO',
},
AccessLogSettings: {
DestinationArn: { "Fn::GetAtt": ["WebSocketLogGroup", "Arn"] },
Format: JSON.stringify({
requestId: '$context.requestId',
connectedAt: '$context.connectedAt',
apiId: '$context.apiId',
requestTime: '$context.requestTime',
stage: '$context.stage',
eventType: '$context.eventType',
routeKey: '$context.routeKey',
connectionId: '$context.connectionId',
messageDirection: '$context.messageDirection',
status: '$context.status',
errorMessage: '$context.error.message',
validationError: '$context.error.validationErrorString',
integrationError: '$context.integrationErrorMessage',
authorizeError: '$context.authorizer.error',
responseLatency: '$context.integrationLatency',
sourceIp: '$context.identity.sourceIp'
})
}
}
},
WebSocketLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/apigatewayv2/${AWS::StackName}-WebSocket' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] }
]
]
},
RetentionInDays: {
'Fn::If': ['LogRetentionPeriodIsNotZero', { Ref: 'LogRetentionPeriod' }, { Ref: 'AWS::NoValue' }]
}
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK')
}
},
WebSocketApiLoggingRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['apigateway.amazonaws.com']
},
Action: ['sts:AssumeRole']
}
]
},
Path: '/',
Policies: [
{
PolicyName: 'WebSocketApiLoggingPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'logs:DescribeLogGroups'
],
Resource: ["*"]
},
{
Effect: 'Allow',
Action: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:DescribeLogStreams',
'logs:PutLogEvents',
'logs:GetLogEvents',
'logs:FilterLogEvents'
],
Resource: [
{
'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*'
}
]
}
]
}
}
]
},
Metadata: {
cfn_nag: util.cfnNag(['W11']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK')
}
},
ApiGatewayAccountConfig: {
Type: 'AWS::ApiGateway::Account',
Properties: {
CloudWatchRoleArn: { 'Fn::GetAtt': ['WebSocketApiLoggingRole', 'Arn'] }
}
}
};
================================================
FILE: source/templates/testall/Makefile
================================================
BUILD=../../bin/build.js
NAME=$(shell basename $(shell pwd))
DST=../../build/templates/$(NAME).json
LAMBDA_DST=../../build/lambda
default: testallstack
testallstack:
$(BUILD) --stack $(NAME) --verbose
================================================
FILE: source/templates/testall/README.md
================================================
# Bulk Document Import
resources for testall stack
================================================
FILE: source/templates/testall/__snapshots__/index.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders testall template correctly 1`] = `
{
"AWSTemplateFormatVersion": "2010-09-09",
"Conditions": {
"LogRetentionPeriodIsNotZero": {
"Fn::Not": [
{
"Fn::Equals": [
{
"Ref": "LogRetentionPeriod",
},
0,
],
},
],
},
"VPCEnabled": {
"Fn::Not": [
{
"Fn::Equals": [
"",
{
"Ref": "VPCSecurityGroupIdList",
},
],
},
],
},
"XRAYEnabled": {
"Fn::Equals": [
{
"Ref": "XraySetting",
},
"TRUE",
],
},
},
"Description": "(SO0189n-testall) QnABot nested testall resources - Version vx.x.x",
"Outputs": {},
"Parameters": {
"AwsSdkLayerLambdaLayer": {
"Type": "String",
},
"BootstrapBucket": {
"Type": "String",
},
"BootstrapPrefix": {
"Type": "String",
},
"CFNInvokePolicy": {
"Type": "String",
},
"CFNLambda": {
"Type": "String",
},
"CommonModulesLambdaLayer": {
"Type": "String",
},
"ContentDesignerOutputBucket": {
"Type": "String",
},
"EsEndpoint": {
"Type": "String",
},
"EsProxyLambda": {
"Type": "String",
},
"LexV2BotAliasId": {
"Type": "String",
},
"LexV2BotId": {
"Type": "String",
},
"LogRetentionPeriod": {
"Type": "Number",
},
"S3Clean": {
"Type": "String",
},
"TestAllBucket": {
"Type": "String",
},
"VPCSecurityGroupIdList": {
"Type": "String",
},
"VPCSubnetIdList": {
"Type": "String",
},
"VarIndex": {
"Type": "String",
},
"XraySetting": {
"Type": "String",
},
},
"Resources": {
"TestAllClean": {
"Properties": {
"Bucket": {
"Ref": "TestAllBucket",
},
"ServiceToken": {
"Ref": "S3Clean",
},
},
"Type": "Custom::S3Clean",
},
"TestAllCodeVersion": {
"Properties": {
"Bucket": {
"Ref": "BootstrapBucket",
},
"BuildDate": Any,
"Key": {
"Fn::Sub": "\${BootstrapPrefix}/lambda/testall.zip",
},
"ServiceToken": {
"Ref": "CFNLambda",
},
},
"Type": "Custom::S3Version",
},
"TestAllRole": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W11",
"reason": "This IAM role requires to have * resource on its permission policy",
},
{
"id": "W12",
"reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray",
},
],
},
"guard": {
"SuppressedRules": [
"IAM_NO_INLINE_POLICY_CHECK",
],
},
},
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"Path": "/",
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":log-group:/aws/lambda/*",
],
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "LambdaFunctionServiceRolePolicy",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":logs:",
{
"Ref": "AWS::Region",
},
":",
{
"Ref": "AWS::AccountId",
},
":log-group:/aws/lambda/*",
],
],
},
},
{
"Action": [
"ec2:CreateNetworkInterface",
"ec2:AssignPrivateIpAddresses",
"ec2:UnassignPrivateIpAddresses",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
},
"PolicyName": "lambdaVPCAccessExecutionRole",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries",
],
"Effect": "Allow",
"Resource": [
"*",
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "xrayDaemonWriteAccess",
},
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Sub": "arn:aws:s3:::\${TestAllBucket}*",
},
{
"Fn::Sub": "arn:aws:s3:::\${ContentDesignerOutputBucket}*",
},
],
},
{
"Action": [
"lambda:InvokeFunction",
],
"Effect": "Allow",
"Resource": [
{
"Ref": "EsProxyLambda",
},
],
},
{
"Action": [
"lex:RecognizeText",
],
"Effect": "Allow",
"Resource": [
{
"Fn::Sub": "arn:\${AWS::Partition}:lex:\${AWS::Region}:\${AWS::AccountId}:bot-alias/*/*",
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "TestAllPolicy",
},
],
},
"Type": "AWS::IAM::Role",
},
"TestAllStepLambda": {
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W92",
"reason": "This lambda function does not require to have ReservedConcurrentExecutions",
},
],
},
"guard": {
"SuppressedRules": [
"LAMBDA_CONCURRENCY_CHECK",
"LAMBDA_INSIDE_VPC",
],
},
},
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "BootstrapBucket",
},
"S3Key": {
"Fn::Sub": "\${BootstrapPrefix}/lambda/testall.zip",
},
"S3ObjectVersion": {
"Ref": "TestAllCodeVersion",
},
},
"Environment": {
"Variables": {
"ES_ENDPOINT": {
"Ref": "EsEndpoint",
},
"ES_INDEX": {
"Ref": "VarIndex",
},
"ES_PROXY": {
"Ref": "EsProxyLambda",
},
"LEXV2_BOT_ALIAS_ID": {
"Ref": "LexV2BotAliasId",
},
"LEXV2_BOT_ID": {
"Ref": "LexV2BotId",
},
"OUTPUT_S3_BUCKET": {
"Ref": "ContentDesignerOutputBucket",
},
"SOLUTION_ID": "SO0189",
"SOLUTION_VERSION": "vx.x.x",
},
},
"Handler": "index.step",
"Layers": [
{
"Ref": "AwsSdkLayerLambdaLayer",
},
{
"Ref": "CommonModulesLambdaLayer",
},
],
"LoggingConfig": {
"LogGroup": {
"Ref": "TestAllStepLambdaLogGroup",
},
},
"MemorySize": "1280",
"Role": {
"Fn::GetAtt": [
"TestAllRole",
"Arn",
],
},
"Runtime": "nodejs",
"Tags": [
{
"Key": "Type",
"Value": "TestAll",
},
],
"Timeout": 900,
"TracingConfig": {
"Fn::If": [
"XRAYEnabled",
{
"Mode": "Active",
},
{
"Ref": "AWS::NoValue",
},
],
},
"VpcConfig": {
"Fn::If": [
"VPCEnabled",
{
"SecurityGroupIds": {
"Fn::Split": [
",",
{
"Ref": "VPCSecurityGroupIdList",
},
],
},
"SubnetIds": {
"Fn::Split": [
",",
{
"Ref": "VPCSubnetIdList",
},
],
},
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Lambda::Function",
},
"TestAllStepLambdaLogGroup": {
"Metadata": {
"guard": {
"SuppressedRules": [
"CLOUDWATCH_LOG_GROUP_ENCRYPTED",
"CW_LOGGROUP_RETENTION_PERIOD_CHECK",
],
},
},
"Properties": {
"LogGroupName": {
"Fn::Join": [
"-",
[
{
"Fn::Sub": "/aws/lambda/\${AWS::StackName}-TestAllStepLambda",
},
{
"Fn::Select": [
"2",
{
"Fn::Split": [
"/",
{
"Ref": "AWS::StackId",
},
],
},
],
},
],
],
},
"RetentionInDays": {
"Fn::If": [
"LogRetentionPeriodIsNotZero",
{
"Ref": "LogRetentionPeriod",
},
{
"Ref": "AWS::NoValue",
},
],
},
},
"Type": "AWS::Logs::LogGroup",
},
"TestAllStepPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"TestAllStepLambda",
"Arn",
],
},
"Principal": "s3.amazonaws.com",
"SourceAccount": {
"Ref": "AWS::AccountId",
},
"SourceArn": {
"Fn::Sub": "arn:aws:s3:::\${TestAllBucket}",
},
},
"Type": "AWS::Lambda::Permission",
},
"TestAllTrigger": {
"Properties": {
"Bucket": {
"Ref": "TestAllBucket",
},
"NotificationConfiguration": {
"LambdaFunctionConfigurations": [
{
"Events": [
"s3:ObjectCreated:*",
],
"Filter": {
"Key": {
"FilterRules": [
{
"Name": "prefix",
"Value": "status",
},
],
},
},
"LambdaFunctionArn": {
"Fn::GetAtt": [
"TestAllStepLambda",
"Arn",
],
},
},
],
},
"ServiceToken": {
"Ref": "CFNLambda",
},
},
"Type": "Custom::S3Lambda",
},
},
}
`;
================================================
FILE: source/templates/testall/bucket.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
TestAllTrigger: {
Type: 'Custom::S3Lambda',
Properties: {
ServiceToken: { Ref: 'CFNLambda' },
Bucket: { Ref: 'TestAllBucket' },
NotificationConfiguration: {
LambdaFunctionConfigurations: [{
LambdaFunctionArn: { 'Fn::GetAtt': ['TestAllStepLambda', 'Arn'] },
Events: ['s3:ObjectCreated:*'],
Filter: {
Key: {
FilterRules: [{
Name: 'prefix',
Value: 'status',
}],
},
},
}],
},
},
},
TestAllStepPermission: {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: { 'Fn::GetAtt': ['TestAllStepLambda', 'Arn'] },
Action: 'lambda:InvokeFunction',
Principal: 's3.amazonaws.com',
SourceAccount: { Ref: 'AWS::AccountId' },
SourceArn: { 'Fn::Sub': 'arn:aws:s3:::${TestAllBucket}' },
},
},
};
================================================
FILE: source/templates/testall/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const fs = require('fs');
const _ = require('lodash');
const files = [
require('./bucket'),
require('./resources'),
];
module.exports = {
Resources: _.assign.apply({}, files),
AWSTemplateFormatVersion: '2010-09-09',
Description: `(SO0189n-testall) QnABot nested testall resources - Version v${process.env.npm_package_version}`,
Outputs: require('./outputs'),
Parameters: {
CFNLambda: { Type: 'String' },
CFNInvokePolicy: { Type: 'String' },
S3Clean: { Type: 'String' },
LexV2BotId: { Type: 'String' },
LexV2BotAliasId: { Type: 'String' },
LogRetentionPeriod: { Type: 'Number' },
BootstrapBucket: { Type: 'String' },
BootstrapPrefix: { Type: 'String' },
VarIndex: { Type: 'String' },
EsEndpoint: { Type: 'String' },
EsProxyLambda: { Type: 'String' },
TestAllBucket: { Type: 'String' },
ContentDesignerOutputBucket: { Type: 'String' },
VPCSubnetIdList: { Type: 'String' },
VPCSecurityGroupIdList: { Type: 'String' },
XraySetting: { Type: 'String' },
AwsSdkLayerLambdaLayer: { Type: 'String' },
CommonModulesLambdaLayer: { Type: 'String' },
},
Conditions: {
VPCEnabled: { 'Fn::Not': [{ 'Fn::Equals': ['', { Ref: 'VPCSecurityGroupIdList' }] }] },
XRAYEnabled: { 'Fn::Equals': [{ Ref: 'XraySetting' }, 'TRUE'] },
LogRetentionPeriodIsNotZero: { 'Fn::Not': [{ 'Fn::Equals': [{ Ref: 'LogRetentionPeriod' }, 0] }] }
},
};
================================================
FILE: source/templates/testall/index.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
function create() {
const file = `${__dirname}/`;
return require(file);
}
it('renders testall template correctly', () => {
const template = create();
expect(template).toMatchSnapshot({
Resources: {
TestAllCodeVersion: {
Properties: {
BuildDate: expect.any(String),
},
},
},
});
});
================================================
FILE: source/templates/testall/outputs.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
};
================================================
FILE: source/templates/testall/resources.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/* eslint-disable quotes */
/* eslint-disable indent */
const fs = require('fs');
const util = require('../util');
module.exports = {
TestAllCodeVersion: {
Type: 'Custom::S3Version',
Properties: {
ServiceToken: { Ref: 'CFNLambda' },
Bucket: { Ref: 'BootstrapBucket' },
Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/testall.zip' },
BuildDate: new Date().toISOString(),
},
},
TestAllStepLambdaLogGroup: {
Type: 'AWS::Logs::LogGroup',
Properties: {
LogGroupName: {
'Fn::Join': [
'-',
[
{ 'Fn::Sub': '/aws/lambda/${AWS::StackName}-TestAllStepLambda' },
{ 'Fn::Select': ['2', { 'Fn::Split': ['/', { Ref: 'AWS::StackId' }] }] },
],
],
},
RetentionInDays: {
'Fn::If': [
'LogRetentionPeriodIsNotZero',
{ Ref: 'LogRetentionPeriod' },
{ Ref: 'AWS::NoValue' },
],
},
},
Metadata: {
guard: util.cfnGuard('CLOUDWATCH_LOG_GROUP_ENCRYPTED', 'CW_LOGGROUP_RETENTION_PERIOD_CHECK'),
},
},
TestAllStepLambda: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'BootstrapBucket' },
S3Key: { 'Fn::Sub': '${BootstrapPrefix}/lambda/testall.zip' },
S3ObjectVersion: { Ref: 'TestAllCodeVersion' },
},
Environment: {
Variables: {
ES_INDEX: { Ref: 'VarIndex' },
ES_ENDPOINT: { Ref: 'EsEndpoint' },
ES_PROXY: { Ref: 'EsProxyLambda' },
LEXV2_BOT_ID: { Ref: 'LexV2BotId' },
LEXV2_BOT_ALIAS_ID: { Ref: 'LexV2BotAliasId' },
OUTPUT_S3_BUCKET: { Ref: 'ContentDesignerOutputBucket'},
...util.getCommonEnvironmentVariables(),
},
},
Handler: 'index.step',
LoggingConfig: {
LogGroup: { Ref: 'TestAllStepLambdaLogGroup' },
},
MemorySize: '1280',
Role: { 'Fn::GetAtt': ['TestAllRole', 'Arn'] },
Runtime: process.env.npm_package_config_lambdaRuntime,
Timeout: 900,
VpcConfig: {
'Fn::If': [
'VPCEnabled',
{
SubnetIds: { 'Fn::Split': [',', { Ref: 'VPCSubnetIdList' }] },
SecurityGroupIds: { 'Fn::Split': [',', { Ref: 'VPCSecurityGroupIdList' }] },
},
{ Ref: 'AWS::NoValue' },
],
},
TracingConfig: {
'Fn::If': ['XRAYEnabled', { Mode: 'Active' }, { Ref: 'AWS::NoValue' }],
},
Layers: [
{ Ref: 'AwsSdkLayerLambdaLayer' },
{ Ref: 'CommonModulesLambdaLayer' },
],
Tags: [
{
Key: 'Type',
Value: 'TestAll',
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W92']),
guard: util.cfnGuard('LAMBDA_CONCURRENCY_CHECK', 'LAMBDA_INSIDE_VPC'),
},
},
TestAllRole: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'lambda.amazonaws.com',
},
Action: 'sts:AssumeRole',
},
],
},
Path: '/',
Policies: [
util.basicLambdaExecutionPolicy(),
util.lambdaVPCAccessExecutionRole(),
util.xrayDaemonWriteAccess(),
{
PolicyName: 'TestAllPolicy',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
's3:PutObject',
's3:GetObject',
's3:GetObjectVersion',
's3:DeleteObject',
's3:DeleteObjectVersion',
],
Resource: [
{ 'Fn::Sub': 'arn:aws:s3:::${TestAllBucket}*' },
{ 'Fn::Sub': 'arn:aws:s3:::${ContentDesignerOutputBucket}*' },
],
},
{
Effect: 'Allow',
Action: ['lambda:InvokeFunction'],
Resource: [{ Ref: 'EsProxyLambda' }],
},
{
Effect: 'Allow',
Action: ['lex:RecognizeText'],
Resource: [
{
'Fn::Sub':
'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/*/*',
},
],
},
],
},
},
],
},
Metadata: {
cfn_nag: util.cfnNag(['W11', 'W12']),
guard: util.cfnGuard('IAM_NO_INLINE_POLICY_CHECK'),
},
},
TestAllClean: {
Type: 'Custom::S3Clean',
Properties: {
ServiceToken: { Ref: 'S3Clean' },
Bucket: { Ref: 'TestAllBucket' },
},
},
};
================================================
FILE: source/templates/util.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.httpsOnlyBucketPolicy = function (bucketName = 'Bucket') {
return {
Type: 'AWS::S3::BucketPolicy',
Properties: {
Bucket: {
Ref: bucketName,
},
PolicyDocument: {
Statement: [
{
Action: '*',
Condition: {
Bool: {
'aws:SecureTransport': 'false',
},
},
Effect: 'Deny',
Principal: '*',
Resource: [
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
bucketName,
'Arn',
],
},
'/*',
],
],
},
{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
bucketName,
'Arn',
],
},
],
],
},
],
Sid: 'HttpsOnly',
},
],
Version: '2012-10-17',
},
},
};
};
exports.basicLambdaExecutionPolicy = function () {
return {
PolicyDocument: {
Statement: [
{
Action: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
Effect: 'Allow',
Resource: {
'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':logs:',
{
Ref: 'AWS::Region',
},
':',
{
Ref: 'AWS::AccountId',
},
':log-group:/aws/lambda/*',
],
],
},
},
],
Version: '2012-10-17',
},
PolicyName: 'LambdaFunctionServiceRolePolicy',
};
};
exports.lambdaVPCAccessExecutionRole = function () {
return {
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
Resource: {
'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':logs:',
{
Ref: 'AWS::Region',
},
':',
{
Ref: 'AWS::AccountId',
},
':log-group:/aws/lambda/*',
],
],
},
},
{ // ec2 permissions for VPC access https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaENIManagementAccess.html
Effect: 'Allow',
Action: [
'ec2:CreateNetworkInterface',
'ec2:AssignPrivateIpAddresses',
'ec2:UnassignPrivateIpAddresses',
'ec2:DescribeNetworkInterfaces',
'ec2:DeleteNetworkInterface',
],
Resource: '*',
},
],
},
PolicyName: 'lambdaVPCAccessExecutionRole',
};
};
exports.xrayDaemonWriteAccess = function () {
return {
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'xray:PutTraceSegments',
'xray:PutTelemetryRecords',
'xray:GetSamplingRules',
'xray:GetSamplingTargets',
'xray:GetSamplingStatisticSummaries',
],
Resource: [
'*',
],
},
],
},
PolicyName: 'xrayDaemonWriteAccess', // https://docs.aws.amazon.com/xray/latest/devguide/security_iam_id-based-policy-examples.html#xray-permissions-managedpolicies
};
};
exports.amazonKendraReadOnlyAccess = function () {
return {
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'kendra:DescribeIndex',
'kendra:ListIndices',
'kendra:Query',
'kendra:GetQuerySuggestions',
],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:kendra:${AWS::Region}:${AWS::AccountId}:index/*' }],
},
],
},
PolicyName: 'amazonKendraReadOnlyAccess',
};
};
exports.translateReadOnly = function () {
return {
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: [
'translate:TranslateText',
'translate:GetTerminology',
'translate:ListTerminologies',
'comprehend:DetectDominantLanguage',
'cloudwatch:GetMetricStatistics',
'cloudwatch:ListMetrics',
],
Effect: 'Allow',
Resource: '*', // these actions cannot be bound to resources other than *
},
],
},
PolicyName: 'translateReadOnly', // https://docs.aws.amazon.com/translate/latest/dg/security-iam-awsmanpol.html#security-iam-awsmanpol-TranslateReadOnly
};
};
exports.lexFullAccess = function () {
return {
PolicyName: 'AWSQnaBotLexFullAccess', // https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonLexFullAccess.html
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'polly:SynthesizeSpeech',
'logs:DescribeLogGroups',
'cloudwatch:DescribeAlarms',
'kms:DescribeKey',
's3:GetBucketLocation',
'lambda:GetPolicy',
],
Resource: [
{ 'Fn::Sub': 'arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:polly:${AWS::Region}:${AWS::AccountId}:lexicon/*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:cloudwatch:${AWS::Region}:${AWS::AccountId}:alarm:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:s3:::*' },
],
},
{
Effect: 'Allow',
Action: [
's3:ListAllMyBuckets',
'lambda:ListFunctions',
'cloudwatch:DescribeAlarmsForMetric',
'kms:ListAliases',
'iam:ListRoles',
'cloudwatch:GetMetricStatistics',
'kendra:ListIndices',
'polly:DescribeVoices',
],
Resource: '*', // these actions cannot be bound to resources other than *
},
{
Effect: 'Allow',
Action: 'lex:*',
Resource: [
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:intent:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:slottype:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-channel:*:*' },
],
},
{ // Lex V2 policies
Effect: 'Allow',
Action: [
'lex:CreateUploadUrl',
'lex:ListBuiltInSlotTypes',
'lex:ListBots',
'lex:ListBuiltInIntents',
'lex:ListImports',
'lex:ListExports',
],
Resource: '*', // these actions cannot be bound to resources other than *
},
{
Effect: 'Allow',
Action: 'lex:*',
Resource: [
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/*/*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot/*' },
],
},
{
Effect: 'Allow',
Action: 'lex:*',
Resource: [
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:intent:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:slottype:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot:*:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot:*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-channel:*:*' },
],
},
{ // Lex V2 policies
Effect: 'Allow',
Action: [
'lex:CreateUploadUrl',
'lex:ListBuiltInSlotTypes',
'lex:ListBots',
'lex:ListBuiltInIntents',
'lex:ListImports',
'lex:ListExports',
],
Resource: '*', // these actions cannot be bound to resources other than *
},
{
Effect: 'Allow',
Action: 'lex:*',
Resource: [
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/*/*' },
{ 'Fn::Sub': 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot/*' },
],
},
{
Effect: 'Allow',
Action: [
'lambda:AddPermission',
'lambda:RemovePermission',
],
Resource: { 'Fn::Sub': 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:AmazonLex*' },
Condition: {
StringEquals: {
'lambda:Principal': 'lex.amazonaws.com',
},
},
},
{
Effect: 'Allow',
Action: [
'iam:GetRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lex.amazonaws.com/AWSServiceRoleForLexBots',
'arn:aws:iam::*:role/aws-service-role/channels.lex.amazonaws.com/AWSServiceRoleForLexChannels',
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
'arn:aws:iam::*:role/aws-service-role/channels.lexv2.amazonaws.com/AWSServiceRoleForLexV2Channels*',
],
},
{
Effect: 'Allow',
Action: [
'iam:CreateServiceLinkedRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lex.amazonaws.com/AWSServiceRoleForLexBots',
],
Condition: {
StringEquals: {
'iam:AWSServiceName': 'lex.amazonaws.com',
},
},
},
{
Effect: 'Allow',
Action: [
'iam:CreateServiceLinkedRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/channels.lex.amazonaws.com/AWSServiceRoleForLexChannels',
],
Condition: {
StringEquals: {
'iam:AWSServiceName': 'channels.lex.amazonaws.com',
},
},
},
{
Effect: 'Allow',
Action: [
'iam:CreateServiceLinkedRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
],
Condition: {
StringEquals: {
'iam:AWSServiceName': 'lexv2.amazonaws.com',
},
},
},
{
Effect: 'Allow',
Action: [
'iam:CreateServiceLinkedRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/channels.lexv2.amazonaws.com/AWSServiceRoleForLexV2Channels*',
],
Condition: {
StringEquals: {
'iam:AWSServiceName': 'channels.lexv2.amazonaws.com',
},
},
},
{
Effect: 'Allow',
Action: [
'iam:DeleteServiceLinkedRole',
'iam:GetServiceLinkedRoleDeletionStatus',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lex.amazonaws.com/AWSServiceRoleForLexBots',
'arn:aws:iam::*:role/aws-service-role/channels.lex.amazonaws.com/AWSServiceRoleForLexChannels',
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
'arn:aws:iam::*:role/aws-service-role/channels.lexv2.amazonaws.com/AWSServiceRoleForLexV2Channels*',
],
},
{
Effect: 'Allow',
Action: [
'iam:PassRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lex.amazonaws.com/AWSServiceRoleForLexBots',
],
Condition: {
StringEquals: {
'iam:PassedToService': [
'lex.amazonaws.com',
],
},
},
},
{
Effect: 'Allow',
Action: [
'iam:PassRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexV2Bots*',
],
Condition: {
StringEquals: {
'iam:PassedToService': [
'lexv2.amazonaws.com',
],
},
},
},
{
Effect: 'Allow',
Action: [
'iam:PassRole',
],
Resource: [
'arn:aws:iam::*:role/aws-service-role/channels.lexv2.amazonaws.com/AWSServiceRoleForLexV2Channels*',
],
Condition: {
StringEquals: {
'iam:PassedToService': [
'channels.lexv2.amazonaws.com',
],
},
},
},
],
},
};
};
exports.esCognitoAccess = function () {
return {
PolicyName: 'AWSQnaBotESCognitoAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'cognito-idp:DescribeUserPool',
'cognito-idp:CreateUserPoolClient',
'cognito-idp:DeleteUserPoolClient',
'cognito-idp:DescribeUserPoolClient',
'cognito-idp:AdminInitiateAuth',
'cognito-idp:AdminUserGlobalSignOut',
'cognito-idp:ListUserPoolClients',
],
Resource: [
{ 'Fn::GetAtt': ['UserPool', 'Arn'] },
],
},
{
Effect: 'Allow',
Action: [
'cognito-identity:DescribeIdentityPool',
'cognito-identity:UpdateIdentityPool',
'cognito-identity:GetIdentityPoolRoles',
],
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:cognito-identity:${AWS::Region}:${AWS::AccountId}:identitypool/*' }],
},
{
Effect: 'Allow',
Action: [
'cognito-identity:SetIdentityPoolRoles',
],
Resource: '*',// these actions cannot be bound to resources other than *
},
{
Effect: 'Allow',
Action: 'iam:PassRole',
Resource: [{ 'Fn::Sub': 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AWS::StackName}-*' }],
Condition: {
StringLike: {
'iam:PassedToService': 'cognito-identity.amazonaws.com',
},
},
},
],
},
};
};
exports.comprehendReadOnly = function () {
return {
PolicyName: 'AWSQnaBotComprehendReadOnly', // https://docs.aws.amazon.com/aws-managed-policy/latest/reference/ComprehendReadOnly.html
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'comprehend:DetectDominantLanguage',
'comprehend:DetectEntities',
'comprehend:DetectKeyPhrases',
'comprehend:DetectPiiEntities',
'comprehend:ContainsPiiEntities',
'comprehend:DetectSentiment',
'comprehend:DetectSyntax',
'comprehend:DescribeEntityRecognizer',
'comprehend:ListEntityRecognizers',
],
Resource: '*', // these actions cannot be bound to resources other than *
},
],
},
};
};
exports.openSearchAccessPolicy = function () {
return {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
AWS: [{ 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] }]
},
Action: [ "es:ESHttp*" ],
Resource: [{
'Fn::Join': [
'',
[
{
'Fn::GetAtt': [
'ESVar',
'ESArn',
],
},
'/*',
],
],
}],
},
],
};
};
exports.advancedSecurityOptions = function (){
return {
Enabled: true,
AnonymousAuthEnabled: false,
InternalUserDatabaseEnabled: false,
MasterUserOptions: {
MasterUserARN: { 'Fn::GetAtt': ['ESProxyLambdaRole', 'Arn'] },
},
};
};
exports.openSearchLogResourcePolicy = function(){
return {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'opensearchservice.amazonaws.com',
},
Action: [
'logs:PutLogEvents',
'logs:CreateLogStream',
],
Resource: ['arn:*:logs:*:*:log-group:/aws/opensearch/*'],
}
]
}
}
exports.streamingPermissions = function () {
return { 'Fn::If': ['StreamingEnabled',
{
PolicyName: 'StreamingPermissions',
PolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'execute-api:Invoke',
'execute-api:ManageConnections',
],
Resource: [{
'Fn::Join': ['', [
'arn:',
{ 'Fn::Sub': '${AWS::Partition}' },
':execute-api:',
{ 'Fn::Sub': '${AWS::Region}' },
':',
{ 'Fn::Sub': '${AWS::AccountId}' },
':',
{ 'Fn::GetAtt': ['StreamingStack', 'Outputs.StreamingWebSocketApiId'] },
'/Prod/*'
]]
}],
},
{
Effect: 'Allow',
Action: [
'dynamodb:GetItem',
],
Resource: [{ 'Fn::GetAtt': ['StreamingStack', 'Outputs.StreamingDynamoDbTableArn'] }],
}
],
},
},
{ Ref: 'AWS::NoValue' }
]};
}
exports.cfnNagXray = function () {
return {
cfn_nag: {
rules_to_suppress: [{
id: 'W12',
reason: 'Lambda needs the following minimum required permissions to send trace data to X-Ray',
}],
},
};
};
exports.cfnNag = function (rules, reason = '') {
const suppressed_rules = {
W11: {
id: 'W11',
reason: 'This IAM role requires to have * resource on its permission policy',
},
W12: {
id: 'W12',
reason: 'Lambda needs the following minimum required permissions to send trace data to X-Ray',
},
W13: {
id: 'W13',
reason: 'This IAM policy requires to have * resource',
},
W35: {
id: 'W35',
reason: 'Access logging is not required for this Bucket.',
},
W57: {
id: 'W57',
reason: 'This IdentityPool has proper restrictions for unauthenticated users',
},
W58: {
id: 'W58',
reason: 'This Lambda already has permission to write cloudwatch logs via CFNLambdaRole',
},
W59: {
id: 'W59',
reason: 'This ApiGateway Method does not need authorization setup',
},
W64: {
id: 'W64',
reason: 'This apiGateway stage does not require to be associated with a usage plan',
},
W69: {
id: 'W69',
reason: 'This apiGateway stage does not require to have access logging',
},
W74: {
id: 'W74',
reason: 'This DynamoDB table does not require CMK encryption store in KMS',
},
W76: {
id: 'W76',
reason: 'This role is required to have high SPCM',
},
W84: {
id: 'W84',
reason: 'LogGroup needs to be retained indefinitely',
},
W86: {
id: 'W86',
reason: 'LogGroup is encrypted by default.',
},
W89: {
id: 'W89',
reason: 'This Lambda Function is not required to be inside VPC',
},
W92: {
id: 'W92',
reason: 'This lambda function does not require to have ReservedConcurrentExecutions',
},
F3: {
id: 'F3',
reason: 'This role policy is required to have * action in its policy',
},
F5: {
id: 'F5',
reason: 'This role policy is required to have * action in its policy',
},
F10: {
id: 'F10',
reason: 'This is a custom role with specific IAM permission needs',
},
F14: {
id: 'F4',
reason: 'ACLs are not used in this S3 bucket',
},
F38: {
id: 'F38',
reason: 'This role policy is required to have * action in its policy with PassRole action',
},
F66: {
id: 'F66',
reason: 'This solution does not use Redshift',
},
F68: {
id: 'F68',
reason: 'This solution does not use Splunk',
},
};
return {
rules_to_suppress: rules.map((rule) => {
const supression = suppressed_rules[rule];
// if caller provides a reason, replace the default message
if (reason) {
supression.reason = reason;
}
return supression;
}),
};
};
exports.cfnGuard = (...rules) => {
if (rules.constructor !== Array || rules.length === 0) {
throw new Error('rules must be a non-empty array');
}
return {
SuppressedRules: [...rules],
};
};
exports.getCommonEnvironmentVariables = function () {
return {
"SOLUTION_ID": "SO0189",
"SOLUTION_VERSION": `v${process.env.npm_package_version}`
};
};
================================================
FILE: source/utility_scripts/README.md
================================================
# Utility Scripts that can be used post deploy to alter QnABot stack
## CMK based setup
Customer security/compliance policies sometimes require AWS KMS CMK to be used to encrypt content/configuration rather
than default AES-256 based keys. The configureCMK.py script allows a user to specify a KMS CMK ARN with which
to encrypt QnABot Lambdas and Parameter Store settings.
Use
python3 configureCMK.py
As an example
```
python3 configureCMK.py us-west-2 QnABotDevStack arn:aws:kms:us-west-2:nnnnnnnnnnnn:key/nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn
```
## CSV2JSON Converter
Utility to help with ingestion of your content in CSV format. See [CSV2JSON_README](./csv2json_converter/CSV2JSON_README.md).
## Conditional Chaining Validator
Validates QNABot export files against the new safe expression evaluator to identify compatibility issues before upgrading.
### Purpose
QNABot has moved from a permissive expression evaluation mechanism to a more restrictive safe evaluator that prevents security vulnerabilities. This script helps you identify which QIDs in your export will be affected by this change before you upgrade.
### What it checks
The script analyzes all QIDs with `conditionalChaining` expressions and validates them against the new security rules:
- No prototype manipulation (`__proto__`, `constructor`, etc.)
- No dynamic property access (bracket notation)
- No assignment operators (`=`, `++`, `--`)
- Only allowed method calls (`includes`, `startsWith`, `endsWith`, `indexOf`, `toLowerCase`, `toUpperCase`, `trim`)
- No standalone function calls
- Only known context identifiers
### Usage
```bash
node validate-conditional-chaining.js
```
### Example
```bash
node validate-conditional-chaining.js qna-export.json
```
### Output
The script provides:
1. **Summary statistics**: Total QIDs, how many use conditional chaining, how many are valid/invalid
2. **Detailed failure report**: For each invalid expression, shows the QID, expression, and specific error
3. **Exit code**: 0 if all expressions are valid, 1 if any failures detected
### When to use
- Before upgrading to a version with the new safe expression evaluator (>v7.3.0)
- After making changes to conditional chaining expressions
- As part of your QNABot content review process
================================================
FILE: source/utility_scripts/configureAlerts.py
================================================
######################################################################################################################
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #
# SPDX-License-Identifier: Apache-2.0 #
######################################################################################################################
import boto3
from botocore.config import Config
import argparse
import json
import base64
import sys
parser = argparse.ArgumentParser(
description='Sets alerts for QnABot Lambdas and OpenSearch Cluster based on stack name')
parser.add_argument("region", help="AWS Region")
parser.add_argument("stack_arn", help="the Name of the QnABot CloudFormation Stack")
parser.add_argument("topic_arn", help="the arn of the topic to send alarms")
args = type('', (), {})()
args = parser.parse_args()
client_config = Config(
region_name=args.region
)
lambda_client = boto3.client('lambda', config=client_config)
cloudformation_client = boto3.client('cloudformation', config=client_config)
cloudwatch = boto3.client('cloudwatch', config=client_config)
target_lambda_logical_ids = [
'FulfillmentLambda',
'ESQueryLambda'
]
def put_alarm_cluster(domainname, desc, metricname, treatmissingdata, topic):
alarm = cloudwatch.put_metric_alarm(
AlarmName='QnABotClusterStatus-' + desc + '-' + domainname,
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=3,
DatapointsToAlarm=3,
MetricName=metricname,
Namespace='AWS/ES',
Period=300,
Statistic='Average',
Threshold=0,
ActionsEnabled=True,
AlarmActions=[topic],
TreatMissingData=treatmissingdata,
AlarmDescription='Alarm when server status exceeds 0',
Dimensions=[
{
'Name': 'DomainName',
'Value': domainname
},
],
Unit='Seconds'
)
def put_alarm_lambda(functionname, desc, metricname, threshold, treatmissingdata, topic):
alarm = cloudwatch.put_metric_alarm(
AlarmName='QnABotLambda-' + desc + '-' + functionname,
ComparisonOperator='GreaterThanThreshold',
EvaluationPeriods=3,
DatapointsToAlarm=3,
MetricName=metricname,
Namespace='AWS/Lambda',
Period=300,
Statistic='Average',
Threshold=threshold,
ActionsEnabled=True,
AlarmActions=[topic],
TreatMissingData=treatmissingdata,
AlarmDescription='Alarm when server status exceeds 0',
Dimensions=[
{
'Name': 'FunctionName',
'Value': functionname
},
],
Unit='Seconds'
)
def process_stacks(stackname):
paginator = cloudformation_client.get_paginator('list_stack_resources')
response_iterator = paginator.paginate(
StackName=stackname,
PaginationConfig={
'MaxItems': 10000 # ,
}
)
for response in response_iterator:
escluster_resources = filter(lambda x: x["ResourceType"] == "AWS::OpenSearchService::Domain",
response["StackResourceSummaries"])
for cluster in escluster_resources:
print(cluster["PhysicalResourceId"])
put_alarm_cluster(cluster["PhysicalResourceId"], 'Red', 'ClusterStatus.red', 'notBreaching', args.topic_arn)
put_alarm_cluster(cluster["PhysicalResourceId"], 'Yellow', 'ClusterStatus.yellow', 'notBreaching', args.topic_arn)
lambda_resources = filter(lambda x: x["ResourceType"] == "AWS::Lambda::Function" and
x["LogicalResourceId"] in target_lambda_logical_ids,
response["StackResourceSummaries"])
for lambda_func in lambda_resources:
print(lambda_func["PhysicalResourceId"])
put_alarm_lambda(lambda_func["PhysicalResourceId"], 'Throttles', 'Throttles', 10, 'notBreaching', args.topic_arn)
put_alarm_lambda(lambda_func["PhysicalResourceId"], 'Errors', 'Errors', 5, 'notBreaching', args.topic_arn)
process_stacks(args.stack_arn)
paginator = cloudformation_client.get_paginator('list_stack_resources')
response_iterator = paginator.paginate(
StackName=args.stack_arn,
PaginationConfig={
'MaxItems': 10000,
}
)
for response in response_iterator:
stacks = filter(lambda x: x["ResourceType"] == "AWS::CloudFormation::Stack", response["StackResourceSummaries"])
for stack in stacks:
print(f"Processing stack {stack['PhysicalResourceId']}")
process_stacks(stack["PhysicalResourceId"])
================================================
FILE: source/utility_scripts/configureCMK.py
================================================
######################################################################################################################
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #
# SPDX-License-Identifier: Apache-2.0 #
######################################################################################################################
import boto3
from botocore.config import Config
import argparse
import json
import base64
import sys
parser = argparse.ArgumentParser(description='Uses a specified CMK to encrypt QnABot Lambdas and Parameter Store settings')
parser.add_argument("region", help="AWS Region")
parser.add_argument("stack_arn", help="the arn of the QnABot CloudFormation Stack")
parser.add_argument("cmk_arn", help="the ARN of the Customer Master Key to use for encryption")
parser.add_argument("target_s3_bucket", help="the Name of the S3 bucket to use for server access logs")
args = type('', (), {})()
args = parser.parse_args()
client_config = Config(
region_name = args.region
)
lambda_client = boto3.client('lambda', config=client_config)
iam_client = boto3.client('iam', config=client_config)
role_paginator = iam_client.get_paginator('list_role_policies')
kms_client = boto3.client("kms", config=client_config)
cloudformation_client = boto3.client('cloudformation', config=client_config)
ssm_client = boto3.client('ssm', config=client_config)
s3_client = boto3.client('s3', config=client_config)
ddb_client = boto3.client('dynamodb', config=client_config)
sts_client = boto3.client('sts', config=client_config)
kinesis_client = boto3.client('firehose', config=client_config)
policy_name = "CMKPolicy4"
policy_document = {
"Version":"2012-10-17",
"Statement":[
{
"Effect":"Allow",
"Action":[
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey"
],
"Resource":args.cmk_arn
}
]
}
cmk_roles_logical_ids = [
'S3AccessRole',
'FirehoseESS3Role',
'AdminRole',
'ExportRole',
'ImportRole',
'ApiGatewayRole',
'ESCognitoRole',
'OpenSearchDashboardsRole',
]
cmk_roles_physical_ids = []
def assign_role(role_name):
role_iterator = role_paginator.paginate(
RoleName=role_name,
PaginationConfig={
'MaxItems': 1000,
'PageSize': 1000
}
)
print(f"Updating role {role_name}...")
cmk_policy_exists = False
for role in role_iterator:
if policy_name in role["PolicyNames"]:
cmk_policy_exists = True
break
if not cmk_policy_exists:
iam_client.put_role_policy(RoleName=role_name, PolicyName = policy_name,PolicyDocument=json.dumps(policy_document))
def put_key_policy (stackname,roles):
response = kms_client.get_key_policy(KeyId = args.cmk_arn, PolicyName='default')
policy = response['Policy'].replace("\n","")
policy = json.loads(policy)
caller_identity = sts_client.get_caller_identity()
new_statement = []
for statement in policy["Statement"]:
if(statement["Sid"] != stackname):
new_statement.append(statement)
policy["Statement"] = new_statement
formatted_roles = []
for role in roles:
formatted_roles.append(f"arn:aws:iam::{caller_identity['Account']}:role/{role}")
policy["Statement"].append(
{
"Sid": stackname,
"Effect": "Allow",
"Principal": {
"AWS": formatted_roles
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": args.cmk_arn
}
)
print(f"Updating policy for key {args.cmk_arn}")
kms_client.put_key_policy(
KeyId = args.cmk_arn,
PolicyName = "default",
Policy = json.dumps(policy)
)
print(f"Policy for key {args.cmk_arn} updated.")
def process_stacks(stackname):
paginator = cloudformation_client.get_paginator('list_stack_resources')
response_iterator = paginator.paginate(
StackName=stackname,
PaginationConfig={
'MaxItems': 10000#,
}
)
for response in response_iterator:
lambda_resources = filter(lambda x: x["ResourceType"] == "AWS::Lambda::Function",response["StackResourceSummaries"])
for lambda_func in lambda_resources:
lambda_client.update_function_configuration(FunctionName=lambda_func["PhysicalResourceId"],KMSKeyArn=args.cmk_arn)
print(f"Updated function {lambda_func['PhysicalResourceId']} in stack {stackname}")
lambda_configuration = lambda_client.get_function_configuration(FunctionName=lambda_func["PhysicalResourceId"])
role_name = lambda_configuration["Role"].split("/")[-1]
assign_role(role_name)
ssm_parameters = filter(lambda x: x["ResourceType"] == "AWS::SSM::Parameter",response["StackResourceSummaries"])
for parameter in ssm_parameters:
parameter_name = parameter["PhysicalResourceId"]
parameter_response = ssm_client.get_parameter(
Name=parameter_name,
WithDecryption=True
)
parameter_value = parameter_response['Parameter']['Value']
description = parameter_response['Parameter']["Description"] if "Decription" in parameter_response['Parameter'] else ""
ssm_client.put_parameter(
Name=parameter_name,
Description=description,
Value=parameter_value,
Type='SecureString',
KeyId=args.cmk_arn,
Overwrite=True,
)
s3_buckets = filter(lambda x: x["ResourceType"] == "AWS::S3::Bucket",response["StackResourceSummaries"])
for bucket in s3_buckets:
s3_client.put_bucket_encryption(
Bucket=bucket["PhysicalResourceId"],
ServerSideEncryptionConfiguration={
'Rules': [
{
'ApplyServerSideEncryptionByDefault': {
'SSEAlgorithm': 'aws:kms',
'KMSMasterKeyID': args.cmk_arn
}
},
]
}
)
print(f"Encryption set for {bucket['PhysicalResourceId']}")
s3_client.put_bucket_logging(
Bucket=bucket["PhysicalResourceId"],
BucketLoggingStatus={
'LoggingEnabled': {
'TargetBucket': args.target_s3_bucket,
'TargetPrefix': bucket["PhysicalResourceId"] + '/'
}
}
)
print(f"Access Logs set for {bucket['PhysicalResourceId']}")
ddb_tables = filter(lambda x: x["ResourceType"] == "AWS::DynamoDB::Table",response["StackResourceSummaries"])
for table in ddb_tables:
table_description = ddb_client.describe_table(TableName = table["PhysicalResourceId"])
if('SSEDescription' not in table_description["Table"] or 'KMSMasterKeyArn' not in table_description["Table"]['SSEDescription'] or table_description["Table"]['SSEDescription']['KMSMasterKeyArn']!= args.cmk_arn ):
ddb_client.update_table(
TableName = table["PhysicalResourceId"],
SSESpecification ={
'Enabled': True,
'SSEType': 'KMS',
'KMSMasterKeyId': args.cmk_arn
}
)
kinesis_streams = filter(lambda x: x["ResourceType"] == "AWS::KinesisFirehose::DeliveryStream",response["StackResourceSummaries"])
for stream in kinesis_streams:
stream_response = kinesis_client.describe_delivery_stream(
DeliveryStreamName=stream["PhysicalResourceId"])
if('KeyType' not in stream_response['DeliveryStreamDescription']['DeliveryStreamEncryptionConfiguration']
or ( stream_response['DeliveryStreamDescription']['DeliveryStreamEncryptionConfiguration']['KeyType'] != "CUSTOMER_MANAGED_CMK"
and stream_response['DeliveryStreamDescription']['DeliveryStreamEncryptionConfiguration']['KeyARN'] != args.cmk_arn)):
kinesis_client.start_delivery_stream_encryption(
DeliveryStreamName=stream["PhysicalResourceId"],
DeliveryStreamEncryptionConfigurationInput={
'KeyARN': args.cmk_arn,
'KeyType': 'CUSTOMER_MANAGED_CMK'})
role_resources = filter(lambda x: 'LambdaRole' in x["LogicalResourceId"] or x["LogicalResourceId"] in cmk_roles_logical_ids , response["StackResourceSummaries"])
for role_resource in role_resources:
print(f"role_resource: {role_resource['PhysicalResourceId']}")
cmk_roles_physical_ids.append(role_resource["PhysicalResourceId"])
assign_role(role_resource["PhysicalResourceId"])
process_stacks(args.stack_arn)
paginator = cloudformation_client.get_paginator('list_stack_resources')
response_iterator = paginator.paginate(
StackName=args.stack_arn,
PaginationConfig={
'MaxItems': 10000,
}
)
for response in response_iterator:
stacks = filter(lambda x: x["ResourceType"] == "AWS::CloudFormation::Stack",response["StackResourceSummaries"])
for stack in stacks:
print(f"Processing stack {stack['PhysicalResourceId']}")
process_stacks(stack["PhysicalResourceId"])
put_key_policy(args.stack_arn,cmk_roles_physical_ids)
================================================
FILE: source/utility_scripts/count_user_interactions.js
================================================
#! /usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
(async () => {
process.env.AWS_SDK_LOAD_CONFIG = true;
const region = process.env.AWS_REGION;
const { DynamoDBClient, ScanCommand } = require('@aws-sdk/client-dynamodb');
const dynamodb = new DynamoDBClient({ region });
args = process.argv.slice(2);
if (args.length != 1) {
console.log('Must specify DynamoDB tablename');
throw 'Must specify DynamoDB tablename';
}
const getAllData = async (params) => {
const _getAllData = async (params, startKey) => {
if (startKey) {
params.ExclusiveStartKey = startKey;
}
const scanCmd = new ScanCommand(params);
return dynamodb.send(scanCmd);
};
let lastEvaluatedKey = null;
let rows = [];
let count = 0;
do {
const result = await _getAllData(params, lastEvaluatedKey);
count += result.Count;
rows = rows.concat(result.Items);
lastEvaluatedKey = result.LastEvaluatedKey;
} while (lastEvaluatedKey);
return { Rows: rows, Count: count };
};
const params = {
ExpressionAttributeValues: {
':count': {
N: '1',
},
':seconds': {
N: `${60 * 60 * 24 * 30}`,
},
},
FilterExpression: 'InteractionCount > :count AND TimeSinceLastInteraction < :seconds',
TableName: args[0],
Select: 'COUNT',
};
const alldata = await getAllData(params);
console.log(`Users with more than one interaction ${alldata.Count}`);
})();
================================================
FILE: source/utility_scripts/create_kendra_faq_resources.js
================================================
#! /usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
(async () => {
process.env.AWS_SDK_LOAD_CONFIG = true;
const { IAMClient, GetPolicyCommand, CreatePolicyCommand, GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand } = require('@aws-sdk/client-iam');
const { KendraClient, CreateIndexCommand } = require('@aws-sdk/client-kendra');
const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts');
const region = process.env.AWS_REGION;
const sts = new STSClient({ region });
const getCallerIdentityCmd = new GetCallerIdentityCommand({});
const account = (await sts.send(getCallerIdentityCmd)).Account;
let policy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['cloudwatch:PutMetricData'],
Resource: '*', // these actions cannot be bound to resources other than *
Condition: {
StringEquals: {
'cloudwatch:namespace': 'AWS/Kendra'
}
}
},
{
Effect: 'Allow',
Action: ['logs:DescribeLogGroups'],
Resource: '*' // these actions cannot be bound to resources other than *
},
{
Effect: 'Allow',
Action: ['logs:CreateLogGroup'],
Resource: [`arn:aws:${region}:${account}:log-group:/aws/kendra/*`]
},
{
Effect: 'Allow',
Action: ['logs:DescribeLogStreams', 'logs:CreateLogStream', 'logs:PutLogEvents'],
Resource: [`arn:aws:logs:${region}:${account}:log-group:/aws/kendra/*:log-stream:*`]
}
]
};
const getPolicyParams = {
PolicyArn: `arn:aws:iam::${account}:policy/AmazonKendra-${region}-QnABot`
};
const iam = new IAMClient({ region });
let doesPolicyExist = false;
try {
const policyCmd = new GetPolicyCommand(getPolicyParams);
policy = await iam.send(policyCmd);
doesPolicyExist = true;
} catch {}
if (!doesPolicyExist) {
const params = {
PolicyDocument: JSON.stringify(policy),
PolicyName: `AmazonKendra-${region}-QnABot`,
Description: 'Policy for Kendra - Created by QnABot'
};
const createPolicyCmd = new CreatePolicyCommand(params);
await iam.send(createPolicyCmd);
}
let doesRoleExist = false;
try {
const getRoleCmd = new GetRoleCommand({ RoleName: `AmazonKendra-${region}-QnaBot` });
await iam.send(getRoleCmd);
doesRoleExist = true;
} catch {}
if (!doesRoleExist) {
const policyDocument = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: 'kendra.amazonaws.com'
},
Action: 'sts:AssumeRole'
}
]
};
const params = {
AssumeRolePolicyDocument: JSON.stringify(policyDocument),
Path: '/',
RoleName: `AmazonKendra-${region}-QnaBot`
};
const createRoleCmd = new CreateRoleCommand(params);
await iam.send(createRoleCmd);
}
const params = {
PolicyArn: `arn:aws:iam::${account}:policy/AmazonKendra-${region}-QnABot`,
RoleName: `AmazonKendra-${region}-QnaBot`
};
const attachRoleCmd = new AttachRolePolicyCommand(params)
await iam.send(attachRoleCmd);
const kendra = new KendraClient({ region });
const indexResult = await kendra.listIndices();
const indexCount = indexResult.IndexConfigurationSummaryItems.length;
let createdIndex = null;
if (indexCount == 0) {
const kendraCreateIndexParams = {
Name: 'QnABot' /* required */,
RoleArn: `arn:aws:iam::${account}:role/AmazonKendra-${region}-QnaBot` /* required */,
Description: 'Created by QnABot',
Edition: 'ENTERPRISE_EDITION'
};
const createIndexCmd = new CreateIndexCommand(kendraCreateIndexParams)
createdIndex = await kendra.send(createIndexCmd).Id;
} else {
console.log('WARNING:Existing Kendra indexes found. Did not create a new index');
}
if (createdIndex) {
console.log(`Add ${createdIndex} to the KENDRA_FAQ_INDEX setting in the Content Designer`);
} else {
console.log('Add one of the following indexes to the KENDRA_FAQ_INDEX setting in the Content Designer');
for (index of indexResult.IndexConfigurationSummaryItems) {
console.log(`${index.Id} ${index.Status}`);
}
}
})();
================================================
FILE: source/utility_scripts/csv2json_converter/CSV2JSON_README.md
================================================
AWS QnABot -- CSV file to JSON converter tool
===============================================
To support easier ingestion of your content in CSV format, we created this tool to help with the ingestion of CSV content into QnABot Designer. Please refer to the CSV input file specifications below.
Input File Specifications
You can get started with your CSV file with just a few fields:
- question_identifier -- a unique identifier for each question.
- question_type -- there are 2 types in QnABot (qna and quiz). This tool supports "qna" type.
- question -- question that your users will ask. This field supports input for 1 question. You can use the QnABot Designer to add more.
- answer -- answer applicable for the {question} field.
- markdown_answer (optional) -- answer applicable for the {question} field in markdown format.
Sample file format: (the last line should be a empty line)
```
question_identifier, question_type, question, answer, markdown_answer
q_1, qna, this is question 1 created in csv, this is answer 1 created in csv
q_2, qna, this is question 2 created in csv, this is answer 2 created in csv, this is **answer 2** in markdown created in csv
q_3, qna, this is question 3 created in csv, "this is answer 3 created in csv, having commas"
```
To start the CSV to JSON conversion process, open the {qnabot_csv2json_converter.html} in Firefox or Chrome browser.
Then follow the steps outlined in the web page.
================================================
FILE: source/utility_scripts/csv2json_converter/css/qnabot_csv2json_converter.css
================================================
h1 {
color: black;
text-align: left;
font-family: calibri;
}
p {
color: black;
text-align: left;
font-family: calibri;
}
.file_spec {
color: black;
text-align: left;
font-family: consolas;
}
.file_selector {
color: black;
text-align: left;
font-family: calibri;
}
button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
.progress_status {
color: black;
text-align: left;
font-family: calibri;
}
#divProgress {
color: blue;
text-align: left;
font-family: consolas;
}
.JSON_output {
color: blue;
text-align: left;
font-family: consolas;
}
================================================
FILE: source/utility_scripts/csv2json_converter/js/csvToArray.v2.1.js
================================================
/* Copyright 2012-2013 Daniel Tillin
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* csvToArray v2.1 (Unminifiled for development)
*
* For documentation visit:
* http://code.google.com/p/csv-to-array/
*
*/
String.prototype.csvToArray = function (o) {
const od = {
fSep: ',',
rSep: '\r\n',
quot: '"',
head: false,
trim: false,
};
if (o) {
for (const i in od) {
if (!o[i]) o[i] = od[i];
}
} else {
o = od;
}
const a = [
[''],
];
for (let r = f = p = q = 0; p < this.length; p++) {
switch (c = this.charAt(p)) {
case o.quot:
if (q && this.charAt(p + 1) == o.quot) {
a[r][f] += o.quot;
++p;
} else {
q ^= 1;
}
break;
case o.fSep:
if (!q) {
if (o.trim) {
a[r][f] = a[r][f].replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
a[r][++f] = '';
} else {
a[r][f] += c;
}
break;
case o.rSep.charAt(0):
if (!q && (!o.rSep.charAt(1) || (o.rSep.charAt(1) && o.rSep.charAt(1) == this.charAt(p + 1)))) {
if (o.trim) {
a[r][f] = a[r][f].replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
a[++r] = [''];
a[r][f = 0] = '';
if (o.rSep.charAt(1)) {
++p;
}
} else {
a[r][f] += c;
}
break;
default:
a[r][f] += c;
}
}
if (o.head) {
a.shift();
}
if (a[a.length - 1].length < a[0].length) {
a.pop();
}
return a;
};
================================================
FILE: source/utility_scripts/csv2json_converter/js/qnabot_csv2json_converter.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
// AWS QnA Bot -- CSV to JSON converter
// more details on AWS QnA Bot project is available by following: https://www.amazon.com/qnabot
// this javascript file contains functions to create a JSON file according to the JSON schema of QnA Bot
let QUESTION_IDENTIFIER_INDEX;
let QUESTION_TYPE_INDEX;
let QUESTION_INDEX;
let QUESTION_ANSWER_INDEX;
let QUESTION_ANSWER_MARKDOWN;
// function to load input CSV file and load into an Array
function load_csv_file() {
// get the selected file
const objfileInputToConvert = document.getElementById('input_file').files[0];
// create and instantiate a new file reader object
const objFileReader = new FileReader();
if (objfileInputToConvert) {
updateProgress('
File selected');
// load the contents of the file to display in a textarea html object
let arrCSVOutput = [];
let strJSONOutput;
objFileReader.onload = function (event) {
// store the file contents in a variable. we will use this for conversion to JSON
updateProgress('
File loaded');
const strFileContents = event.target.result;
arrCSVOutput = strFileContents.csvToArray({ fSep: ',', trim: true, quot: '"' }); // load the CSV file into an Array
strJSONOutput = convertToQnABotJSON(arrCSVOutput); // convert the CSV file to QnABot JSON format
// document.getElementById("inputTextToSave").value = strJSONOutput;
};
objFileReader.onerror = function (event) {
alert(event.target.error.name);
};
objFileReader.readAsText(objfileInputToConvert, 'UTF-8'); // read the file
} else {
updateProgress('
No file selected');
}
}
// function to convert the CSV file input to QnABot JSON format
function convertToQnABotJSON(arrCSVInput) {
let strJSONOutput = '{"qna": [{'; // start the JSON structure
updateProgress(`
Number of rows found (including header row): ${arrCSVInput.length}`);
updateProgress('
Conversion to JSON in progress...');
// check if the input csv file has the needed fields. If not, return exception and stop conversion
if (!checkCSVFileFormat(arrCSVInput[0])) {
return false;
}
for (let intRow = 1; intRow < (arrCSVInput.length); intRow++) { // iterate through the array object (skip header row)
if (intRow > 1) {
strJSONOutput += '}, {';
}
for (let intCol = 0; intCol <= arrCSVInput[intRow].length; intCol++) { // iterate through each array column for each row
if (intCol == QUESTION_IDENTIFIER_INDEX) {
strJSONOutput += create_QnA_qid(arrCSVInput[intRow][intCol]); // create the QnA qid JSON key
}
if (intCol == QUESTION_TYPE_INDEX) {
strJSONOutput += create_QnA_type(arrCSVInput[intRow][intCol]); // create the QnA type JSON key
}
if (intCol == QUESTION_INDEX) {
strJSONOutput += create_QnA_q(arrCSVInput[intRow][intCol]); // create the QnA q JSON key
}
if (intCol == QUESTION_ANSWER_INDEX) {
strJSONOutput += create_QnA_a(arrCSVInput[intRow][intCol]); // create the QnA a JSON key
}
if (intCol == QUESTION_ANSWER_MARKDOWN && arrCSVInput[intRow][intCol]) { // create the QnA alt+markdown JSON key
strJSONOutput += create_QnA_alt_markdown(arrCSVInput[intRow][intCol]);
}
if (arrCSVInput[intRow][intCol] && intCol < arrCSVInput[intRow].length - 1) { // add a comma if more JSON key pairs are to be added
strJSONOutput += ', ';
}
}
}
strJSONOutput += '}]}'; // close the JSON structure
strJSONOutput = strJSONOutput.replace(/""/g, '"'); // replace globally for any occurence of two double-quotes
try {
if (JSON.parse(strJSONOutput) && typeof JSON.parse(strJSONOutput) === 'object') {
saveConvertedFile(strJSONOutput); // create converted file and provide dialog box to save the file
}
} catch (e) {
console.log(e);
updateProgress('
ERROR: Input file does not meet file format specifications.
Please check your input file headers and/or remove any end-of-line commas and try again. ');
}
return (strJSONOutput);
}
// function to check if the input csv file meets the file format specifications
function checkCSVFileFormat(arrFieldHeadersinFile) {
let blnError = false;
for (let intFieldHeaderinFileCounter = 0; intFieldHeaderinFileCounter < arrFieldHeadersinFile.length; intFieldHeaderinFileCounter++) {
if (blnError) {
break;
}
switch (arrFieldHeadersinFile[intFieldHeaderinFileCounter]) {
case 'question_identifier':
QUESTION_IDENTIFIER_INDEX = intFieldHeaderinFileCounter;
break;
case 'question_type':
QUESTION_TYPE_INDEX = intFieldHeaderinFileCounter;
break;
case 'question':
QUESTION_INDEX = intFieldHeaderinFileCounter;
break;
case 'answer':
QUESTION_ANSWER_INDEX = intFieldHeaderinFileCounter;
break;
case 'markdown_answer':
QUESTION_ANSWER_MARKDOWN = intFieldHeaderinFileCounter;
break;
default:
updateProgress('
ERROR: Input file does not meet file format specifications. Please check your input file headers and try again. ');
blnError = true;
}
}
if (blnError) {
return false;
}
return true;
}
// function to create the JSON file and provide option for user to save the created JSON file
// this JSON file can then be imported using the AWS QnA Bot designer console
function saveConvertedFile(strJSONOutput) {
const textToSaveAsBlob = new Blob([strJSONOutput], { type: 'text/json' });
const textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);
const fileNameToSaveAs = 'qnaCSVtoJSON.json';
const downloadLink = document.createElement('a');
downloadLink.download = fileNameToSaveAs;
downloadLink.innerText = 'Download File';
downloadLink.href = textToSaveAsURL;
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
updateProgress('
Conversion complete. Download/Save file when prompted');
downloadLink.click();
}
// function to create the qid JSON key
function create_QnA_qid(strInputCSVValue) {
strOutput = '"qid":' + `"${strInputCSVValue.trim()}"`;
return strOutput;
}
// function to create the type JSON key
function create_QnA_type(strInputCSVValue) {
strOutput = '"type":' + `"${strInputCSVValue.trim()}"`;
return strOutput;
}
// function to create the q JSON key
function create_QnA_q(strInputCSVValue) {
strOutput = '"q":[' + `"${strInputCSVValue.trim()}"]`;
return strOutput;
}
// function to create the a JSON key
function create_QnA_a(strInputCSVValue) {
strOutput = '"a":' + `"${strInputCSVValue.trim()}"`;
return strOutput;
}
// function to create the alt+markdown JSON key
function create_QnA_alt_markdown(strInputCSVValue) {
strOutput = '"alt":{"markdown":' + `"${strInputCSVValue.trim()}"}`;
return strOutput;
}
// function to show progress message
function updateProgress(strProgressMsg) {
document.getElementById('divProgress').innerText = document.getElementById('divProgress').innerText + strProgressMsg;
}
================================================
FILE: source/utility_scripts/csv2json_converter/qnabot_csv2json_converter.html
================================================
AWS QnA Bot -- CSV to JSON converter tool
AWS QnABot -- CSV file to JSON converter tool
To support easier ingestion of your content in CSV format, we created this tool to help with the ingestion of CSV content into QnA Bot Designer.
Please refer to the CSV input file specifications below.
Input File Specifications
You can get started with your CSV file with just a few fields:
- question_identifier -- a unique identifier for each question.
- question_type -- there are 2 types in QnA Bot (qna and quiz). This tool supports "qna" type.
- question -- question that your users will ask. This field supports input for 1 question. You can use the QnA Bot Designer to add more.
- answer -- answer applicable for the {question} field.
- markdown_answer (optional) -- answer applicable for the {question} field in markdown format.
Sample file format: (the last line should be a empty line)
question_identifier, question_type, question, answer, markdown_answer
q_1, qna, this is question 1 created in csv, this is answer 1 created in csv
q_2, qna, this is question 2 created in csv, this is answer 2 created in csv, this is **answer 2** in markdown created in csv
q_3, qna, this is question 3 created in csv, "this is answer 3 created in csv, having commas"
|
================================================
FILE: source/utility_scripts/csv2json_converter/sample.csv
================================================
question_identifier, question_type, question, answer, markdown_answer
q_1, qna, this is question 1 created in csv, this is answer 1 created in csv
q_2, qna, this is question 2 created in csv, this is answer 2 created in csv, this is **answer 2** in markdown created in csv
q_3, qna, this is question 3 created in csv, "this is answer 3 created in csv, having commas"
================================================
FILE: source/utility_scripts/migration.md
================================================
### On your local computer
brew install git
export $(aws cloudformation describe-stacks --stack-name $OUT_STACK --output text --query 'Stacks[0].Outputs[].join(`=`, [join(`_`, [`CF`, `OUT`, OutputKey]), OutputValue ])')
export $(aws cloudformation describe-stacks --stack-name develop-branch-dev-dev-master-10 --output text --query 'Stacks[0].Outputs[].join(`=`, [join(`_`, [`CF`, `IN`, OutputKey]), OutputValue ])')
sudo yum install
git clone https://github.com/aws-solutions/qnabot-on-aws.git
cd qnabot-on-aws/
git branch kendra_translate
npm install
npm run config
nano config.json
- Set namespace to blank
- Change devEmail
npm run bootstrap
npm run up
### On CloudShell
export $(aws cloudformation describe-stacks --stack-name develop-branch-dev-dev-master-12 --output text --query 'Stacks[0].Outputs[].join(`=`, [join(`_`, [`CF`, `OUT`, OutputKey]), OutputValue ])')
export $(aws cloudformation describe-stacks --stack-name develop-branch-dev-dev-master-10 --output text --query 'Stacks[0].Outputs[].join(`=`, [join(`_`, [`CF`, `IN`, OutputKey]), OutputValue ])')
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'
echo "export PATH=~/.npm-global/bin:\$PATH" > ~/.profile
source ~/.profile
npm install -g elasticdump
elasticdump --input=https://$CF_IN_ElasticsearchEndpoint --output=https://CF_OUT_ElasticsearchEndpoint --type=data --awsChain
### In Kibana
GET /_cat/indices
POST _reindex
{
"source": {
"index": "develop-branch-dev-dev-master-10_20210206_020224"
},
"dest": {
"index": "develop-branch-dev-dev-master-12_20210209_230238"
}
}
### Or from the command line
aws-es-curl -X POST https://$CF_OUT_ElasticsearchEndpoint/_reindex -d '{"source": {"index": "develop-branch-dev-dev-master-10-metrics_20210206_020224"},"dest": {"index": "develop-branch-dev-dev-master-12-metrics_20210209_230238"}}' --region us-east-1
Unable to access metadata service. EC2 Metadata roleName request returned error
{"took":75,"timed_out":false,"total":6,"updated":6,"created":0,"deleted":0,"batches":1,"version_conflicts":0,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1.0,"throttled_until_millis":0,"failures":[]}
================================================
FILE: source/utility_scripts/validate-conditional-chaining.js
================================================
#!/usr/bin/env node
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/**
* Validation script for QnABot conditional chaining expressions
*
* This script analyzes a QnABot export file to identify QIDs that use conditional
* chaining expressions and validates them against the new safe expression evaluator.
*
* Usage:
* node validate-conditional-chaining.js
*
* Example:
* node validate-conditional-chaining.js qna-export.json
*/
const fs = require('fs');
const path = require('path');
// Import the safe expression evaluator directly from the Lambda layer
const { tokenize, validateTokens } = require(path.join(__dirname, '../lambda/es-proxy-layer/lib/fulfillment-event/safeExpressionEvaluator'));
// Constants
const REPORT_WIDTH = 80;
/**
* Create a generic context object for validation
* This matches the typical QnABot runtime context
*/
function createGenericContext() {
return {
SessionAttributes: {},
Question: '',
Sentiment: 0,
UserInfo: {},
Settings: {},
LexOrAlexa: '',
event: {},
req: {},
res: {}
};
}
/**
* Check if the conditional chaining string is a Lambda function
*/
function isLambdaExpression(conditionalChaining) {
if (!conditionalChaining || typeof conditionalChaining !== 'string') {
return false;
}
const trimmed = conditionalChaining.trim();
// Check for Lambda functions
// Handles: Lambda::, "Lambda::, 'Lambda::, `Lambda::, with optional whitespace
return /^['"`]?\s*lambda::/i.test(trimmed);
}
/**
* Validate a single conditional chaining expression
*/
function validateExpression(expression, qid) {
const context = createGenericContext();
const result = {
qid,
expression,
valid: false,
error: null
};
try {
const tokens = tokenize(expression);
validateTokens(tokens, context);
result.valid = true;
} catch (error) {
result.valid = false;
result.error = error.message;
}
return result;
}
/**
* Validate QnABot export data structure and expressions
* Returns validation results without printing
*/
function validateExportFile(data) {
// Validate file structure
if (!data || typeof data !== 'object') {
throw new Error('Invalid export file format. Expected JSON object.');
}
if (!data.qna || !Array.isArray(data.qna)) {
throw new Error('Invalid export file format. Expected { "qna": [...] }');
}
const qids = data.qna;
const results = {
total: qids.length,
withChaining: 0,
lambdaChaining: 0,
validExpressions: 0,
invalidExpressions: 0,
failures: []
};
// Process each QID
qids.forEach((item, index) => {
// Validate QID structure
if (!item || typeof item !== 'object') {
console.warn(`Warning: Skipping invalid QID at index ${index} (not an object)`);
return;
}
if (!item.qid) {
console.warn(`Warning: Skipping QID at index ${index} (missing 'qid' property)`);
return;
}
const conditionalChaining = item.conditionalChaining;
if (!conditionalChaining || typeof conditionalChaining !== 'string') {
return;
}
results.withChaining++;
// Skip Lambda functions
if (isLambdaExpression(conditionalChaining)) {
results.lambdaChaining++;
return;
}
const expression = conditionalChaining.trim();
// Validate the expression
const validationResult = validateExpression(expression, item.qid);
if (validationResult.valid) {
results.validExpressions++;
} else {
results.invalidExpressions++;
results.failures.push(validationResult);
}
});
return results;
}
/**
* Print validation report to console
*/
function printReport(results, filePath) {
console.log(`\n${'='.repeat(REPORT_WIDTH)}`);
console.log('QnABot Conditional Chaining Validation Report');
console.log(`${'='.repeat(REPORT_WIDTH)}\n`);
console.log(`Analyzing: ${filePath}\n`);
// Print summary
console.log('SUMMARY');
console.log(`${'-'.repeat(REPORT_WIDTH)}`);
console.log(`Total QIDs: ${results.total}`);
console.log(`QIDs with conditional chaining: ${results.withChaining}`);
console.log(` - Lambda functions (not validated): ${results.lambdaChaining}`);
console.log(` - Valid expressions: ${results.validExpressions}`);
console.log(` - INVALID expressions: ${results.invalidExpressions}`);
console.log();
// Print detailed failures
if (results.invalidExpressions > 0) {
console.log(`\n${'='.repeat(REPORT_WIDTH)}`);
console.log('FAILED VALIDATIONS - ACTION REQUIRED');
console.log(`${'='.repeat(REPORT_WIDTH)}\n`);
results.failures.forEach((failure, index) => {
console.log(`${index + 1}. QID: ${failure.qid}`);
console.log(` Expression: ${failure.expression}`);
console.log(` Error: ${failure.error}`);
console.log();
});
console.log(`${'='.repeat(REPORT_WIDTH)}`);
console.log('RECOMMENDATION');
console.log(`${'-'.repeat(REPORT_WIDTH)}`);
console.log('The expressions above will FAIL with the new safe evaluator.');
console.log('Please review and update these QIDs before upgrading.');
console.log(`${'='.repeat(REPORT_WIDTH)}\n`);
} else {
console.log(`${'='.repeat(REPORT_WIDTH)}`);
console.log('✓ ALL CONDITIONAL CHAINING EXPRESSIONS ARE VALID');
console.log(`${'='.repeat(REPORT_WIDTH)}\n`);
console.log('Your QnABot export is compatible with the new safe evaluator.');
console.log('You can proceed with the upgrade.\n');
}
}
/**
* Process QnABot export file - orchestrates reading, validation, and reporting
*/
function processExportFile(filePath) {
// Read and parse the export file
let data;
try {
let fileContent = fs.readFileSync(filePath, 'utf8');
// Remove BOM if present
if (fileContent.charCodeAt(0) === 0xFEFF) {
fileContent = fileContent.slice(1);
}
data = JSON.parse(fileContent);
} catch (error) {
console.error(`ERROR: Failed to read or parse file: ${error.message}`);
process.exit(1);
}
// Validate the export data
let results;
try {
results = validateExportFile(data);
} catch (error) {
console.error(`ERROR: ${error.message}`);
process.exit(1);
}
// Print the report
printReport(results, filePath);
// Exit with appropriate code
process.exit(results.invalidExpressions > 0 ? 1 : 0);
}
/**
* Print help message
*/
function printHelp() {
console.log(`
QnABot Conditional Chaining Validation Tool
USAGE:
node validate-conditional-chaining.js
node validate-conditional-chaining.js --help
DESCRIPTION:
Validates conditional chaining expressions in a QnABot export file against
the new safe expression evaluator. Identifies expressions that will fail
after upgrading to the secure evaluator.
ARGUMENTS:
Path to QnABot export JSON file
OPTIONS:
--help, -h Show this help message
EXAMPLES:
node validate-conditional-chaining.js qna-export.json
node validate-conditional-chaining.js ./exports/production-backup.json
EXIT CODES:
0 All expressions are valid
1 One or more expressions failed validation or error occurred
`);
}
// Main execution
if (require.main === module) {
const args = process.argv.slice(2);
// Handle help flag
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
printHelp();
process.exit(args.length === 0 ? 1 : 0);
}
const filePath = path.resolve(args[0]);
if (!fs.existsSync(filePath)) {
console.error(`ERROR: File not found: ${filePath}`);
process.exit(1);
}
processExportFile(filePath);
}
module.exports = { validateExpression, createGenericContext, validateExportFile, printReport };
================================================
FILE: source/website/.babelrc
================================================
{
"presets": [
[
"@babel/preset-env"
]
]
}
================================================
FILE: source/website/.gitignore
================================================
test/compiled.js
================================================
FILE: source/website/Makefile
================================================
SOURCES:=$(shell find . -type f -not -path "./build/*")
.PHONY: build test dev
../build/website.zip:$(SOURCES)
export NODE_OPTIONS=--openssl-legacy-provider; export NODE_ENV=prod; make build -B
dev:$(SOURCES)
export NODE_OPTIONS=--openssl-legacy-provider; export NODE_ENV=dev; make build -B
build:
cd ..
npm i webpack-merge -D
npx webpack-cli --config ./config/webpack.config.js
test:
cd ..
npm i webpack-merge -D
npx webpack-cli --config ./config/test.config.js
================================================
FILE: source/website/README.md
================================================
# Designer and Client UI Websites
Builds designer UI and client UI pages
## Running unit tests
In order to run the unit test for the website, go to the solution's home directory then run the npm command to launch the unit test
```
cd ../ # If you are currently in the website/ folder
npm run test:website
```
The unit test configuration can be found in the **package.json** file in the solution's home directory. The unit test uses jest and so its configuration can be found under the **jest** attribute in the package.json file.
================================================
FILE: source/website/__tests__/admin.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import adminModule from '../js/admin.vue';
import { shallowMount } from '@vue/test-utils';
describe('js admin module', () => {
test('mounted', () => {
const wrapper = shallowMount(adminModule);
expect(wrapper.exists()).toBe(true);
});
test('computed methods', () => {
const store = {
state: {
route: {
name: 'test-name',
},
error: 'no-error',
user: {
name: 'test-username',
},
info: {
Version: '1.0.0',
BuildDate: 'test-date',
_links: {
DesignerLogin: {
href: 'test-href1',
},
ClientLogin: {
href: 'test-href2',
},
OpenSearchDashboards: {
href: 'test-href3',
},
},
},
},
};
const wrapper = shallowMount(adminModule, {
global: {
mocks: {
$store: store,
},
},
});
const expectedPages = [{
title: 'Edit',
id: 'edit',
subTitle: 'Edit questions and simulate responses',
icon: 'mode_edit',
href: '#/edit',
}, {
title: 'Settings',
id: 'settings',
subTitle: 'View and Modify QnABot configuration settings',
icon: 'settings',
href: '#/settings',
}, {
title: 'Import',
id: 'import',
subTitle: 'Import new questions',
icon: 'cloud_upload',
href: '#/import',
},
{
title: 'Export',
id: 'export',
subTitle: 'Download backups of your QnAs',
icon: 'file_download',
href: '#/export',
}, {
title: 'Import Custom Terminology',
id: 'customTranslate',
subTitle: 'Import custom translation terminology',
icon: 'transform',
href: '#/customTranslate',
},
{
title: 'Kendra Web Crawler',
id: 'kendraIndexing',
subTitle: 'Crawl web pages with Kendra',
icon: 'search',
href: '#/kendraIndex',
},
{
title: 'Alexa',
id: 'alexa',
subTitle: 'Instructions for setting up an Alexa Skill',
icon: 'info',
href: '#/alexa',
},
{
title: 'Connect',
id: 'connect',
subTitle: 'Instructions for integrating with Connect',
icon: 'info',
href: '#/connect',
},
{
title: 'Genesys Cloud',
id: 'genesys',
subTitle: 'Instructions for integrating with Genesys Cloud',
icon: 'info',
href: '#/genesys',
},
{
title: 'Lambda Hooks',
id: 'hooks',
subTitle: 'Instructions for customizing QnABot behavior using AWS Lambda',
icon: 'info',
href: '#/hooks',
}, {
title: 'QnABot Client',
id: 'client',
subTitle: 'Use QnABot to interact with your bot in the browser',
icon: 'forum',
target: '_blank',
href: 'test-href2',
}, {
title: 'OpenSearch Dashboards',
id: 'openSearchDashboard',
subTitle: 'Analyze ChatBot usage',
icon: 'show_chart',
target: '_blank',
href: 'test-href3',
}];
expect(wrapper.vm.page).toEqual('test-name');
expect(wrapper.vm.error).toEqual('no-error');
expect(wrapper.vm.username).toEqual('test-username');
expect(wrapper.vm.Version).toEqual('1.0.0');
expect(wrapper.vm.BuildDate).toEqual('test-date');
expect(wrapper.vm.login).toEqual('test-href1');
expect(wrapper.vm.client).toEqual('test-href2');
expect(wrapper.vm.pages).toEqual(expectedPages);
});
test('logout', () => {
const store = {
dispatch: jest.fn(),
};
const wrapper = shallowMount(adminModule, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.logout();
expect(store.dispatch).toHaveBeenCalledWith('user/logout');
});
});
================================================
FILE: source/website/__tests__/admin.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require('../js/admin');
const vueRouter = require('vue-router');
jest.mock('vuetify/iconsets/md', () => ({
aliases: jest.fn(),
md: jest.fn(),
}));
jest.mock('vuetify', () => ({
createVuetify: jest.fn(),
}));
jest.mock('vuetify/components', () => {});
jest.mock('vuetify/directives', () => {});
jest.mock('vue-router', () => ({
createRouter: jest.fn().mockReturnValue({
replace: jest.fn(),
isReady: jest.fn().mockReturnValue(Promise.resolve(false)),
}),
}));
jest.mock('vuex-router-sync', () => ({
sync: jest.fn(),
}));
jest.mock('../js/lib', () => ({
router: {},
}));
describe('js admin module', () => {
test('load', () => {
expect(vueRouter.createRouter().replace).toHaveBeenCalledWith('/loading');
});
});
================================================
FILE: source/website/__tests__/client.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import clientModule from '../js/client.vue';
import { shallowMount } from '@vue/test-utils';
describe('js client module', () => {
test('mounted', () => {
const wrapper = shallowMount(clientModule);
expect(wrapper.exists()).toBe(true);
});
});
================================================
FILE: source/website/__tests__/client.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
require('../js/client');
jest.mock('vuetify/iconsets/md', () => ({
aliases: {},
md: {},
}));
jest.mock('vuetify/components', () => {});
jest.mock('vuetify/directives', () => {});
jest.mock('vuetify', () => ({
createVuetify: jest.fn(),
}));
jest.mock('axios', () => ({
head: jest.fn().mockReturnValue({
headers: { 'api-stage': 'dev' },
}),
get: jest.fn().mockReturnValue({
data: {
PoolId: 'test-pool-id',
BotName: 'test-bot-name',
BotVersion: 'test-bot-version',
v2BotId: 'test-bot-id',
v2BotAliasId: 'test-bot-alias-id',
v2BotLocaleId: 'test-bot-locale-id',
},
}),
}));
jest.mock('../js/lib/client-auth', () => (() => ({
username: 'test-username',
idtoken: 'test-idtoken',
config: {
credentials: {
expiration: (Date.now() + 5000),
},
},
lex: {},
polly: {},
})));
jest.mock('aws-lex-web-ui/dist/lex-web-ui.min.js', () => ({
Store: {
dispatch: jest.fn(),
},
}));
describe('js client module', () => {
test('DOMContentLoaded', async () => {
await document.dispatchEvent(new Event('DOMContentLoaded', {
bubbles: false,
cancelable: true,
}));
});
});
================================================
FILE: source/website/__tests__/components/alexa/index.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import alexaModule from '../../../js/components/alexa/index.vue';
import { shallowMount } from '@vue/test-utils';
describe('alexa index component', () => {
let store;
let wrapper;
beforeEach(() => {
jest.resetAllMocks();
store = {
dispatch: jest.fn().mockImplementation(() => {
return Promise.resolve({ result: {} });
}),
state: {
bot: {
LambdaArn: 'some-lambda-arn'
}
}
};
wrapper = shallowMount(alexaModule, {
global: {
mocks: {
$store: store
}
}
});
});
test('should mount', () => {
expect(wrapper.exists()).toBe(true);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('data/botinfo');
});
});
================================================
FILE: source/website/__tests__/components/connect/index.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import connectModule from '../../../js/components/connect/index.vue';
import { shallowMount } from '@vue/test-utils';
describe('connect index component', () => {
test('should mount', () => {
const wrapper = shallowMount(connectModule);
expect(wrapper.exists()).toBe(true);
});
test('copy method', async () => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve('test')),
};
const wrapper = shallowMount(connectModule, {
global: {
mocks: {
$store: store,
},
},
});
const btn = {
loading: false,
};
await wrapper.vm.copy(btn);
expect(store.dispatch).toHaveBeenCalledWith('api/getContactFlow');
});
});
================================================
FILE: source/website/__tests__/components/customTranslate.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/**
* @jest-environment jsdom
*/
import customTranslateModule from '../../js/components/customTranslate.vue'
import { shallowMount } from '@vue/test-utils'
describe('customTranslate component', () => {
const dispatchMock = (dispatchType) => {
switch (dispatchType) {
case 'api/listSettings':
return [
{},
{},
{ ENABLE_CUSTOM_TERMINOLOGY: 'true' },
];
case 'api/getTerminologies':
return Promise.resolve({ result: {} });
case 'api/startImportTranslate':
return Promise.resolve({ result: {} });
default:
return Promise.resolve({
Status: 'success',
Error: '',
});
}
};
const shallowMountWithDefaults = () => {
const store = {
dispatch: jest.fn().mockImplementation((dispatchType) => dispatchMock(dispatchType)),
};
const wrapper = shallowMount(customTranslateModule, {
global: {
mocks: {
$store: store,
},
},
});
return { wrapper, store };
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, "log").mockImplementation(jest.fn());
});
test('should mount', async () => {
const { wrapper, store } = shallowMountWithDefaults();
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledTimes(3);
expect(store.dispatch).toHaveBeenCalledWith('api/listExamples');
expect(store.dispatch).toHaveBeenCalledWith('api/getTerminologies');
expect(store.dispatch).toHaveBeenCalledWith('api/listSettings');
expect(wrapper.exists()).toBe(true);
});
test('CustomTerminologyIsEnabled', async () => {
const { wrapper } = shallowMountWithDefaults();
const result = await wrapper.vm.CustomTerminologyIsEnabled();
expect(result).toBe(true);
});
test('close', () => {
const { wrapper } = shallowMountWithDefaults();
wrapper.vm.$data.loading = true;
wrapper.vm.$data.error = true;
wrapper.vm.close();
expect(wrapper.vm.$data.loading).toBe(false);
expect(wrapper.vm.$data.error).toBe(false);
});
test('refresh', async () => {
const { wrapper, store } = shallowMountWithDefaults();
await wrapper.vm.refresh();
expect(store.dispatch).toHaveBeenCalledWith('api/getTerminologies');
});
test('upload data resolve', async() => {
const data = {
Status: 'success',
Error: ''
};
const nam = 'test-name.txt';
const { wrapper, store } = shallowMountWithDefaults();
await wrapper.vm.upload(data, nam);
expect(store.dispatch).toHaveBeenCalledTimes(4);
expect(store.dispatch).toHaveBeenCalledWith('api/startImportTranslate', {
name: 'test-name',
description: null,
file: window.btoa(data),
});
});
});
================================================
FILE: source/website/__tests__/components/designer/add.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import add from '../../../js/components/designer/add.vue';
import { mount } from '@vue/test-utils';
describe('add vue', () => {
const store = {
dispatch: jest.fn(),
state: {
data: {
schema: {
qna: {},
},
},
},
};
beforeEach(() => {
jest.resetAllMocks();
});
test('It is there', () => {
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.exists()).toBe(true);
});
test('computed methods', () => {
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.types).toEqual(['qna']);
expect(wrapper.vm.schema).toEqual({});
expect(wrapper.vm.required).toEqual([]);
});
test('cancel', () => {
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.cancel();
expect(wrapper.vm.$data.loading).toEqual(false);
expect(wrapper.vm.$data.dialog).toEqual(false);
});
test('add -- data exists', async () => {
store.dispatch.mockReturnValueOnce(Promise.resolve(true));
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.data = {
qna: {
qid: '1',
},
};
await wrapper.vm.add();
expect(store.dispatch).toHaveBeenCalledWith('api/check', '1');
});
test('add -- markdown sanitized as expected', async () => {
store.dispatch.mockReturnValueOnce(Promise.resolve(false));
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.data = {
qna: {
qid:"test",
q:["test"],
a:"test",
alt:{
markdown:"Testing Markdown Sanitization
"
},
type:"qna"
}
};
await wrapper.vm.add();
wrapper.vm.$data.data.qna.alt.markdown = "Testing Markdown Sanitization
";
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'api/check', 'test');
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'data/add', {
"a": "test",
"alt": {
"markdown": "Testing Markdown Sanitization
"
},
"q": ["test"],
"qid": "test",
"type": "qna"
});
});
test('add -- markdown sanitized makes multiple passes', async () => {
store.dispatch.mockReturnValueOnce(Promise.resolve(false));
const wrapper = mount(add, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.data = {
qna: {
qid:"test",
q:["test"],
a:"test",
alt:{
markdown:"ipt>"
},
type:"qna"
}
};
await wrapper.vm.add();
wrapper.vm.$data.data.qna.alt.markdown = "";
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'api/check', 'test');
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'data/add', {
"a": "test",
"alt": {
"markdown": "ipt>"
},
"q": ["test"],
"qid": "test",
"type": "qna"
});
});
});
================================================
FILE: source/website/__tests__/components/designer/addSetting.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import addSettingModule from '../../../js/components/designer/addSetting.vue';
import { mount } from '@vue/test-utils';
describe('addSetting vue', () => {
test('It is there', () => {
const wrapper = mount(addSettingModule);
expect(wrapper.exists()).toBe(true);
});
});
================================================
FILE: source/website/__tests__/components/designer/alexa.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import alexaModule from '../../../js/components/designer/alexa.vue';
import { shallowMount } from '@vue/test-utils';
describe('addSetting vue', () => {
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
},
});
test('It is there', () => {
const wrapper = shallowMount(alexaModule);
expect(wrapper.exists()).toBe(true);
});
test('download', async () => {
const store = {
dispatch: jest.fn(),
state: {
bot: {
alexa: '',
},
},
};
const wrapper = shallowMount(alexaModule, {
global: {
mocks: {
$store: store,
}
}
});
await wrapper.vm.download();
expect(store.dispatch).toHaveBeenCalledWith('data/botinfo');
expect(wrapper.vm.$data.ready).toBe(true);
});
test('copy', async () => {
const store = {};
const wrapper = shallowMount(alexaModule, {
global: {
mocks: {
$store: store,
}
}
});
wrapper.vm.$data.text = 'test';
await wrapper.vm.copy();
expect( navigator.clipboard.writeText).toHaveBeenCalledWith('test');
expect(wrapper.vm.$data.ready).toBe(false);
});
});
================================================
FILE: source/website/__tests__/components/designer/delete.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import deleteModule from '../../../js/components/designer/delete.vue';
import { shallowMount } from '@vue/test-utils';
describe('delete vue', () => {
test('mounted', () => {
const store = {
state: {
data: {
QAs: [
{ select: true, qid: '1' },
],
filter: 'test-filter',
},
},
};
const wrapper = shallowMount(deleteModule, {
global: {
mocks: {
$store: store,
}
}
});
wrapper.vm.$data.dialog = true;
const qas = wrapper.vm.QAs;
const filter = wrapper.vm.filter;
expect(wrapper.exists()).toBe(true);
expect(qas[0].qid).toBe('1');
expect(filter).toBe('test-filter');
});
test('cancel', () => {
const wrapper = shallowMount(deleteModule);
wrapper.vm.$data.dialog = true;
wrapper.vm.cancel();
expect(wrapper.vm.$data.dialog).toBe(false);
});
test('rm', () => {
const store = {
state: {
data: {
QAs: [
{ select: true, qid: '1' },
],
},
},
};
const wrapper = shallowMount(deleteModule, {
global: {
mocks: {
$store: store,
}
}
});
wrapper.vm.rm();
expect(wrapper.emitted('handleDelete')).toBeTruthy();
});
});
================================================
FILE: source/website/__tests__/components/designer/display.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import displayModule from '../../../js/components/designer/display.vue';
import { shallowMount } from '@vue/test-utils';
describe('display vue', () => {
test('mounted', () => {
const wrapper = shallowMount(displayModule);
expect(wrapper.exists()).toBeTruthy();
});
test('empty array', () => {
const wrapper = shallowMount(displayModule, {
props: {
schema: {
type: 'array',
},
modelValue: [],
},
});
expect(wrapper.vm.empty).toBe(true);
});
test('empty array', () => {
const wrapper = shallowMount(displayModule, {
props: {
schema: {
type: 'array',
},
modelValue: [],
},
});
expect(wrapper.vm.empty).toBe(true);
});
test('empty object', () => {
const wrapper = shallowMount(displayModule, {
props: {
schema: {
type: 'object',
},
modelValue: [],
},
});
expect(wrapper.vm.empty).toBe(true);
});
test('empty truthy', () => {
const wrapper = shallowMount(displayModule, {
props: {
schema: {
type: 'boolean',
},
modelValue: false,
},
});
expect(wrapper.vm.empty).toBe(true);
});
test('properties', () => {
const schema = {
type: 'object',
properties: {
key: {},
},
};
const modelValue = {
key: 'value',
};
const expectedResult = [{ name: 'key' }];
const wrapper = shallowMount(displayModule, {
props: {
schema,
modelValue,
},
});
const result = wrapper.vm.properties;
expect(result).toEqual(expectedResult);
});
});
================================================
FILE: source/website/__tests__/components/designer/edit.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import editModule from '../../../js/components/designer/edit.vue';
import { shallowMount } from '@vue/test-utils';
describe('designer edit module', () => {
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
const shallowMountWithTmpData = () => {
const data = {
type: 'qna',
qid: '1',
tmp: {
quniqueterms: 'some-test-terms',
},
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
key: { type: 'string' },
},
},
},
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
return wrapper;
};
test('mounted', () => {
const wrapper = shallowMount(editModule);
expect(wrapper.exists()).toBe(true);
});
test('computed properties', () => {
const data = {
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
key: 'value',
required: true,
},
},
},
},
};
const wrapper = shallowMount(editModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.type).toEqual('qna');
expect(wrapper.vm.schema).toEqual({ key: 'value', required: true });
expect(wrapper.vm.required).toBe(true);
});
test('computed properties -- default data type', () => {
const data = {};
const store = {
state: {
data: {
schema: {
qna: {
key: 'value',
required: true,
},
},
},
},
};
const wrapper = shallowMount(editModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.type).toEqual('qna');
expect(wrapper.vm.schema).toEqual({ key: 'value', required: true });
expect(wrapper.vm.required).toBe(true);
});
test('cancel', () => {
const wrapper = shallowMount(editModule);
wrapper.vm.$data.dialog = true;
wrapper.vm.$data.opened = true;
wrapper.vm.$data.error = 'Some error';
wrapper.vm.cancel();
expect(wrapper.vm.$data.dialog).toBe(false);
expect(wrapper.vm.$data.opened).toBe(false);
expect(wrapper.vm.$data.error).toBe('');
});
test('cancel resets opened flag so data refreshes on next open', () => {
const data = {
type: 'qna',
qid: 'original-id',
q: ['original question'],
a: 'original answer',
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string' },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string' },
type: { type: 'string' },
},
},
},
},
},
};
const wrapper = shallowMount(editModule, {
props: { data },
global: {
mocks: {
$store: store,
},
},
});
// First open - should initialize tmp
wrapper.vm.refresh();
expect(wrapper.vm.$data.opened).toBe(true);
expect(wrapper.vm.$data.tmp.qid).toBe('original-id');
// User makes edits
wrapper.vm.$data.tmp.qid = 'modified-id';
expect(wrapper.vm.$data.tmp.qid).toBe('modified-id');
// User cancels
wrapper.vm.cancel();
expect(wrapper.vm.$data.opened).toBe(false);
// User reopens - should refresh from original data
wrapper.vm.refresh();
expect(wrapper.vm.$data.opened).toBe(true);
expect(wrapper.vm.$data.tmp.qid).toBe('original-id');
});
test('close', () => {
const wrapper = shallowMount(editModule);
wrapper.vm.close();
expect(wrapper.emitted('filter')).toBeTruthy();
});
test('refresh', () => {
const data = {
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
key: 'value',
required: true,
},
},
},
},
};
const wrapper = shallowMount(editModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.opened = false;
wrapper.vm.refresh();
expect(wrapper.vm.$data.opened).toBe(true);
});
test('update', () => {
const wrapper = shallowMountWithTmpData();
wrapper.vm.$data.tmp = {
quniqueterms: 'some-test-terms',
};
wrapper.vm.update();
expect(wrapper.vm.update).not.toThrow();
});
test('update -- empty tmp', () => {
const wrapper = shallowMountWithTmpData();
wrapper.vm.update();
expect(wrapper.vm.update).not.toThrow();
});
test('update -- markdown sanitized', async () => {
const data = {
qid:"test",
q:["test"],
a:"test",
alt:{
markdown:"Testing Markdown Sanitization
"
},
type:"qna",
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string' },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string' },
alt: { type: 'object' },
type: { type: 'string' },
},
},
},
},
tmp: {
quniqueterms: 'some-test-terms',
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.tmp = {
quniqueterms: 'some-test-terms',
qid:"test",
q:["test"],
a:"test",
alt:{
markdown:"Testing Markdown Sanitization
"
},
type:"qna",
};
await wrapper.vm.update();
expect(store.dispatch).toHaveBeenCalledWith('data/update', {
qid:"test",
q:["test"],
a:"test",
alt:{
markdown:"Testing Markdown Sanitization
"
},
type:"qna",
});
});
describe('validation in update method', () => {
test('update prevents submission when validation fails', async () => {
const data = {
qid: 'test',
q: ['test'],
a: 'test',
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string', maxLength: 100 },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string', maxLength: 8000 },
type: { type: 'string' },
},
},
},
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: { data },
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.tmp = {
qid: 'test',
q: ['test'],
a: 'x'.repeat(8001), // Exceeds 8000 character limit
type: 'qna',
};
await wrapper.vm.update();
// Should set error and not dispatch update
expect(wrapper.vm.$data.error).toBeTruthy();
expect(store.dispatch).not.toHaveBeenCalledWith('data/update', expect.anything());
});
test('update allows submission when validation passes', async () => {
const data = {
qid: 'test',
q: ['test'],
a: 'test',
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string', maxLength: 100 },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string', maxLength: 8000 },
type: { type: 'string' },
},
},
},
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: { data },
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.tmp = {
qid: 'test',
q: ['test'],
a: 'valid answer text',
type: 'qna',
};
await wrapper.vm.update();
// Should not set error and should dispatch update
expect(wrapper.vm.$data.error).toBe('');
expect(store.dispatch).toHaveBeenCalledWith('data/update', expect.objectContaining({
qid: 'test',
a: 'valid answer text',
}));
});
test('update shows error message when character limit exceeded', async () => {
const data = {
qid: 'test',
q: ['test'],
a: 'test',
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string', maxLength: 10 },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string', maxLength: 8000 },
type: { type: 'string' },
},
},
},
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: { data },
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.tmp = {
qid: 'this_is_a_very_long_qid_that_exceeds_limit',
q: ['test'],
a: 'test',
type: 'qna',
};
await wrapper.vm.update();
// Should set error message
expect(wrapper.vm.$data.error).toBeTruthy();
expect(typeof wrapper.vm.$data.error).toBe('string');
});
test('update respects valid flag from form', async () => {
const data = {
qid: 'test',
q: ['test'],
a: 'test',
type: 'qna',
};
const store = {
state: {
data: {
schema: {
qna: {
type: 'object',
properties: {
qid: { type: 'string', maxLength: 100 },
q: { type: 'array', items: { type: 'string' } },
a: { type: 'string', maxLength: 8000 },
type: { type: 'string' },
},
},
},
},
},
dispatch: jest.fn().mockReturnValue(false),
};
const wrapper = shallowMount(editModule, {
props: { data },
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.$data.valid = false;
wrapper.vm.$data.tmp = {
qid: 'test',
q: ['test'],
a: 'test',
type: 'qna',
};
await wrapper.vm.update();
// Should not proceed when valid is false
expect(store.dispatch).not.toHaveBeenCalledWith('data/update', expect.anything());
});
});
});
================================================
FILE: source/website/__tests__/components/designer/empty.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const empty = require('../../../js/components/designer/empty');
describe('designer empty helper function', () => {
test('empty', () => {
const testString = {
type: 'string',
value: 'some-value',
};
const emptyString = empty(testString);
expect(emptyString).toBe('');
const testBoolean = {
type: 'boolean',
value: true,
};
const emptyBoolean = empty(testBoolean);
expect(emptyBoolean).toBe(false);
const testArray = {
type: 'array',
items: ['1'],
}
const emptyArray = empty(testArray);
expect(emptyArray).toEqual([]);
const testObject = {
type: 'object',
key: 'value',
};
const emptyObject = empty(testObject);
expect(emptyObject).toEqual({});
});
});
================================================
FILE: source/website/__tests__/components/designer/event-bus.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const eventBus = require('../../../js/components/designer/event-bus');
describe('designer event bus', () => {
test('event bus', () => {
expect(eventBus).toBeDefined();
});
});
================================================
FILE: source/website/__tests__/components/designer/index.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const indexModule = require('../../../js/components/designer/index.vue');
import { shallowMount } from '@vue/test-utils';
describe('designer index component', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('it mounted', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({}))
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
expect(wrapper.exists()).toBe(true);
});
test('computed properties', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
loading: true,
QAs: [{ select: true }]
},
page: {
total: 100
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
expect(wrapper.vm.loading).toBe(true);
expect(wrapper.vm.QAs).toEqual([{ select: true }]);
expect(wrapper.vm.total).toBe(100);
expect(wrapper.vm.empty).toEqual({ empty: false });
expect(wrapper.vm.selectedMultiple).toBe(true);
});
test('loadItems & refresh', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
loading: true,
QAs: [{ select: true }]
},
page: {
total: 100
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.loadItems({ page: 5, sortBy: 'asc' });
expect(wrapper.vm.$data.page).toEqual(5);
expect(wrapper.vm.$data.sortBy).toEqual('asc');
});
test('refresh', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
loading: true,
QAs: [{ select: true }]
},
page: {
total: 100
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.loadItems({ page: 5, sortBy: 'asc' });
wrapper.vm.refresh();
expect(wrapper.vm.$data.itemsPerPage).toEqual(100);
expect(wrapper.vm.$data.page).toEqual(1);
expect(wrapper.vm.$data.sortBy).toEqual([]);
});
describe('checkSelect', () => {
test('should keep selectAll false when checking an item but not all items are selected', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
QAs: [{ select: false }, { select: false }, { select: false }]
},
page: {
total: 3
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.$data.selectAll = false;
wrapper.vm.checkSelect(true);
expect(wrapper.vm.$data.selectAll).toBe(false);
});
test('should set selectAll to true when checking an item and all items are selected', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
QAs: [{ select: true }, { select: true }, { select: true }]
},
page: {
total: 3
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.$data.selectAll = false;
wrapper.vm.checkSelect(true);
expect(wrapper.vm.$data.selectAll).toBe(true);
});
test('should keep selectAll false when checking an item but some items are unselected', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
QAs: [{ select: true }, { select: false }, { select: true }]
},
page: {
total: 3
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.$data.selectAll = false;
wrapper.vm.checkSelect(true);
expect(wrapper.vm.$data.selectAll).toBe(false);
});
test('should set selectAll to false when unchecking any item', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
state: {
data: {
QAs: [{ select: true }, { select: true }, { select: true }]
},
page: {
total: 3
}
}
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.$data.selectAll = true;
wrapper.vm.checkSelect(false);
expect(wrapper.vm.$data.selectAll).toBe(false);
});
});
test('toggleSelectAll', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
commit: jest.fn()
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.toggleSelectAll();
expect(store.commit).toHaveBeenCalledWith('data/selectAll', wrapper.vm.$data.selectAll);
});
test('deleteClose', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({}))
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.deleteClose();
expect(wrapper.vm.$data.deleteLoading).toEqual(false);
expect(wrapper.vm.$data.deleteError).toEqual('');
expect(wrapper.vm.$data.deleteSuccess).toEqual('');
expect(wrapper.vm.$data.deleteIds).toEqual([]);
});
test('handleDelete', async () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
commit: jest.fn().mockImplementation(() => Promise.resolve({}))
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store
}
}
});
const questions = [{ qid: 1 }];
await wrapper.vm.handleDelete(questions);
expect(wrapper.vm.deleteSuccess).toEqual('Success!');
wrapper.vm.$data.selectAll = true;
wrapper.vm.$data.deleteSuccess = '';
await wrapper.vm.handleDelete(questions);
expect(wrapper.vm.deleteSuccess).toEqual('Success!');
});
});
================================================
FILE: source/website/__tests__/components/designer/input.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const inputModule = require('../../../js/components/designer/input.vue');
import { shallowMount } from '@vue/test-utils'
describe('designer input module', () => {
const shallowMountWithDefaults = () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: [
{ key: 'value1' },
{ key: 'value2' },
],
required: true,
index: '1',
name: 'test-name',
schema: {
type: 'object',
items: { type: 'object', key: 'value3' },
},
path: 'qna.test-name[1]',
},
});
return wrapper;
};
test('mounted', () => {
const wrapper = shallowMount(inputModule);
expect(wrapper.exists()).toBe(true);
});
test('computed properties', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = {
maxLength: 10,
title: 'Books',
properties: {
key: {},
},
};
expect(wrapper.vm.singularTitle).toEqual('Book');
expect(wrapper.vm.properties).toEqual([]);
expect(wrapper.vm.validate).toEqual('required|max:10');
expect(wrapper.vm.id).toEqual('qna-test-name-1');
});
test('remove method', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.remove();
expect(wrapper.vm.modelValue).toEqual([{ key: 'value2' }]);
});
test('add method', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.add();
expect(wrapper.vm.modelValue).toEqual([
{ key: 'value1' },
{ key: 'value2' },
{},
]);
});
test('reset method', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.reset();
expect(wrapper.vm.$data.local).toEqual({});
});
test('ifRequired method', () => {
const wrapper = shallowMountWithDefaults();
expect(wrapper.vm.ifRequired()).toEqual(false);
});
test('isValid method', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.isValid(true)
expect(wrapper.vm.$data.valid).toEqual(true);
});
test('setValid method', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.setValid(true);
expect(wrapper.vm.$data.valid).toEqual(false);
});
describe('validation rules', () => {
test('maxLength rule returns true when under limit', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { maxLength: 100 };
const result = wrapper.vm.$data.rules.maxLength('short text');
expect(result).toBe(true);
});
test('maxLength rule returns error string when over limit', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { maxLength: 10 };
const longText = 'this is a very long text that exceeds the limit';
const result = wrapper.vm.$data.rules.maxLength(longText);
expect(result).toBe('Maximum 10 characters allowed');
});
test('maxLength rule returns true when no maxLength defined', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = {};
const result = wrapper.vm.$data.rules.maxLength('any text');
expect(result).toBe(true);
});
test('maxLength rule returns true when value is empty', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { maxLength: 10 };
const result = wrapper.vm.$data.rules.maxLength('');
expect(result).toBe(true);
});
test('schema rule returns true when validation passes', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { type: 'string', maxLength: 100 };
const result = wrapper.vm.$data.rules.schema('valid text');
expect(result).toBe(true);
});
test('schema rule returns error string when maxLength exceeded', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { type: 'string', maxLength: 10 };
const longText = 'this is a very long text that exceeds the limit';
const result = wrapper.vm.$data.rules.schema(longText);
expect(result).toContain('Maximum 10 characters allowed');
});
test('schema rule converts AJV errors to readable strings', () => {
const wrapper = shallowMountWithDefaults();
wrapper.vm.$data.schema = { type: 'string', maxLength: 5 };
const result = wrapper.vm.$data.rules.schema('too long');
expect(typeof result).toBe('string');
expect(result).not.toBe(true);
});
test('required rule returns true for valid non-empty string', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: 'test',
required: true,
schema: { type: 'string' },
},
});
const result = wrapper.vm.$data.rules.required('valid text');
expect(result).toBe(true);
});
test('required rule returns error for empty string when required', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: '',
required: true,
schema: { type: 'string' },
},
});
const result = wrapper.vm.$data.rules.required('');
expect(result).toBe('Required');
});
test('required rule returns true for empty string when not required', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: '',
required: false,
schema: { type: 'string' },
},
});
const result = wrapper.vm.$data.rules.required('');
expect(result).toBe(true);
});
test('required rule returns true for boolean values', () => {
const wrapper = shallowMountWithDefaults();
const result = wrapper.vm.$data.rules.required(false);
expect(result).toBe(true);
});
test('noSpace rule returns error when qid contains spaces', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: 'test id',
schema: { type: 'string', name: 'qid' },
},
});
const result = wrapper.vm.$data.rules.noSpace('test id');
expect(result).toBe('No Spaces Allowed');
});
test('noSpace rule returns true when qid has no spaces', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: 'testid',
schema: { type: 'string', name: 'qid' },
},
});
const result = wrapper.vm.$data.rules.noSpace('testid');
expect(result).toBe(true);
});
test('noSpace rule returns true for non-qid fields', () => {
const wrapper = shallowMount(inputModule, {
props: {
modelValue: 'test value',
schema: { type: 'string', name: 'other' },
},
});
const result = wrapper.vm.$data.rules.noSpace('test value');
expect(result).toBe(true);
});
});
});
================================================
FILE: source/website/__tests__/components/designer/menu-questions.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const menuQuestionsModule = require('../../../js/components/designer/menu-questions.vue');
import { shallowMount } from '@vue/test-utils'
describe('designer menu-questions module', () => {
test('mounted', () => {
const wrapper = shallowMount(menuQuestionsModule);
expect(wrapper.exists()).toBe(true);
});
test('filter', () => {
const wrapper = shallowMount(menuQuestionsModule);
wrapper.vm.filter();
expect(wrapper.emitted()).toBeTruthy();
});
test('refresh', () => {
const wrapper = shallowMount(menuQuestionsModule);
wrapper.vm.refresh();
expect(wrapper.emitted('refresh')).toBeTruthy();
});
test('build', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
}
const wrapper = shallowMount(menuQuestionsModule, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.build();
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('data/build');
});
});
================================================
FILE: source/website/__tests__/components/designer/menu-test.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const menuTestModule = require('../../../js/components/designer/menu-test.vue');
import { shallowMount } from '@vue/test-utils';
describe('designer menu test module', () => {
test('mounted', () => {
const store = {
dispatch: jest.fn(),
};
const wrapper = shallowMount(menuTestModule, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.exists()).toBe(true);
});
test('simulate', () => {
const store = {
dispatch: jest.fn(),
};
const wrapper = shallowMount(menuTestModule, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.simulate();
expect(store.dispatch).toHaveBeenCalledWith('data/search', {
client_filter: '',
query: '',
score_answer: undefined,
score_on: 'qna item questions',
topic: '',
});
});
});
================================================
FILE: source/website/__tests__/components/designer/qa.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const qaModule = require('../../../js/components/designer/qa.vue');
import { shallowMount } from '@vue/test-utils';
describe('designer qa module', () => {
test('mounted', () => {
const wrapper = shallowMount(qaModule);
expect(wrapper.exists()).toBe(true);
});
test('computed properties', () => {
const store = {
state: {
data: {
schema: {
qna: 'test-data',
}
}
}
}
const data = {};
const wrapper = shallowMount(qaModule, {
props: {
data,
},
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.type).toEqual('qna');
expect(wrapper.vm.schema).toEqual('test-data');
expect(wrapper.vm.extra).toEqual(false);
expect(wrapper.vm.items).toEqual({});
expect(wrapper.vm.topitems).toEqual({});
expect(wrapper.vm.bottomitems).toEqual({});
});
});
================================================
FILE: source/website/__tests__/components/designer/rebuild.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const rebuildModule = require('../../../js/components/designer/rebuild.vue');
import { shallowMount } from '@vue/test-utils';
describe('designer rebuild module', () => {
test('mounted', () => {
const wrapper = shallowMount(rebuildModule);
expect(wrapper.exists()).toBe(true);
});
test('computed properties', () => {
const store = {
state: {
bot: {
status: 'not ready',
build: {
message: 'test-message',
},
},
},
};
const wrapper = shallowMount(rebuildModule, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.vm.message).toEqual('test-message');
expect(wrapper.vm.status).toEqual('not ready');
});
test('build & cancel mthods', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
};
const wrapper = shallowMount(rebuildModule, {
global: {
mocks: {
$store: store,
},
},
});
wrapper.vm.build();
wrapper.vm.cancel();
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith('data/build');
});
});
================================================
FILE: source/website/__tests__/components/designer/synckendra.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const synckendraModule = require('../../../js/components/designer/synckendra.vue');
import { shallowMount } from '@vue/test-utils';
describe('designer synckendra module', () => {
const dispatchMock = (dispatchType) => {
switch (dispatchType) {
case 'api/listSettings':
return [
{},
{},
{ KENDRA_FAQ_INDEX: 'test-index' }, //settings
];
case 'api/listExports':
return {
jobs: [],
};
case 'api/getExportByJobId':
return 'Sync Complete';
default:
return Promise.resolve({});
};
};
test('mounted', () => {
const store = {
dispatch: jest.fn().mockImplementation((dispatchType) => dispatchMock(dispatchType)),
}
const wrapper = shallowMount(synckendraModule, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.exists()).toBe(true);
});
test('refresh', async () => {
const store = {
dispatch: jest.fn().mockImplementation((dispatchType) => dispatchMock(dispatchType)),
}
const wrapper = shallowMount(synckendraModule, {
global: {
mocks: {
$store: store,
},
},
});
await wrapper.vm.refresh();
expect(store.dispatch).toHaveBeenCalledWith('api/listExports');
expect(store.dispatch).toHaveBeenCalledWith('api/getExportByJobId', 'qna-kendra-faq.txt');
});
test('start', async() => {
const store = {
dispatch: jest.fn().mockImplementation((dispatchType) => dispatchMock(dispatchType)),
};
const wrapper = shallowMount(synckendraModule, {
global: {
mocks: {
$store: store,
},
},
});
await wrapper.vm.start();
expect(store.dispatch).toHaveBeenCalledWith('api/startKendraSyncExport', {
name: 'qna-kendra-faq.txt',
filter: '',
});
});
});
================================================
FILE: source/website/__tests__/components/export.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import exportModule from '../../js/components/export.vue';
import { shallowMount } from '@vue/test-utils'
describe('export component', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, "log").mockImplementation(jest.fn());
});
test('should mount', async () => {
const store = {
dispatch: jest.fn(),
};
const exportsJobs = { jobs: ['job1'] };
store.dispatch
.mockImplementationOnce(() => Promise.resolve(exportsJobs))
.mockImplementationOnce(() => Promise.resolve('info'))
.mockImplementationOnce(() => Promise.resolve({ status: 'Completed' }));
const wrapper = shallowMount(exportModule, {
global: {
mocks: {
$store: store
}
}
});
expect(wrapper.exists()).toBe(true);
});
test('poll once more', () => {
const store = {
dispatch: jest.fn(),
};
const exportsJobs = { jobs: ['job1'] };
store.dispatch
.mockImplementationOnce(() => Promise.resolve(exportsJobs))
.mockImplementationOnce(() => Promise.resolve('info'))
.mockImplementationOnce(() => Promise.resolve({ status: 'In-Progress' }))
.mockImplementationOnce(() => Promise.resolve({ status: 'Completed' }));
const wrapper = shallowMount(exportModule, {
global: {
mocks: {
$store: store
}
}
});
expect(wrapper.exists()).toBe(true);
});
});
================================================
FILE: source/website/__tests__/components/genesys/index.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import indexModule from '../../../js/components/genesys/index.vue';
import { shallowMount } from '@vue/test-utils';
describe('genesys component index', () => {
test('mounted', () => {
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve({})),
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store,
},
},
});
expect(wrapper.exists()).toBe(true);
});
test('copy method', async () => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
const store = {
dispatch: jest.fn().mockImplementation(() => Promise.resolve('test')),
};
const wrapper = shallowMount(indexModule, {
global: {
mocks: {
$store: store,
},
},
});
const btn = {
loading: false,
};
await wrapper.vm.copy(btn);
expect(store.dispatch).toHaveBeenCalledWith('api/getGenesysCallFlow');
});
});
================================================
FILE: source/website/__tests__/components/hooks/index.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import indexComponent from '../../../js/components/hooks/index.vue';
import { shallowMount } from '@vue/test-utils';
const handlebars = require('handlebars');
jest.mock('handlebars');
// We are mocking the following files this way to prevent jest from
// attempting to read the file contents.
jest.mock('../../../js/components/hooks/codepy.txt', () => jest.fn());
jest.mock('../../../js/components/hooks/codejs.txt', () => jest.fn());
describe('index hook component', () => {
const store = {
state: {
bot: ''
}
};
beforeEach(() => {
jest.resetAllMocks();
});
test('should render', () => {
handlebars.compile.mockReturnValue(jest.fn().mockImplementation(() => 'a = b + c'));
const wrapper = shallowMount(indexComponent, {
global: {
mocks: {
$store: store,
},
}
});
expect(wrapper.exists()).toBe(true);
});
});
================================================
FILE: source/website/__tests__/components/import.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import importModule from '../../js/components/import.vue';
import { shallowMount } from '@vue/test-utils';
describe('import component', () => {
let store;
let wrapper;
let consoleLogSpy;
const shallowMountWithDefaultStore = () => {
store.dispatch.mockImplementation(() => Promise.resolve({}));
wrapper = shallowMount(importModule, {
global: {
mocks: {
$store: store
}
}
});
};
beforeEach(() => {
jest.resetAllMocks();
consoleLogSpy = jest.spyOn(console, "log")
consoleLogSpy.mockImplementation(jest.fn());
store = {
dispatch: jest.fn(),
};
});
test('created and refresh', async () => {
shallowMountWithDefaultStore();
expect(store.dispatch).toHaveBeenCalledTimes(2);
expect(store.dispatch).toHaveBeenCalledWith('api/listImports');
expect(store.dispatch).toHaveBeenCalledWith('api/listExamples');
});
test('delete', () => {
shallowMountWithDefaultStore();
const jobIndexToRemove = 0;
const job = {
loading: false,
id: 'job1',
};
wrapper.vm.$data.jobs = [job];
wrapper.vm.deleteJob(jobIndexToRemove);
expect(store.dispatch).toHaveBeenCalledWith('api/deleteImport', job);
});
test('close', () => {
shallowMountWithDefaultStore();
wrapper.vm.$data.loading = true;
wrapper.vm.$data.error = true;
wrapper.vm.$data.errorMsg = 'test error message';
wrapper.vm.close();
expect(wrapper.vm.$data.loading).toBe(false);
expect(wrapper.vm.$data.error).toBe(false);
expect(wrapper.vm.$data.errorMsg).toEqual('');
});
test('importExample, Geturl, upload', () => {
const mockedData = {
qna: [{}]
};
store.dispatch
.mockImplementationOnce(() => Promise.resolve({})) // refresh
.mockImplementationOnce(() => Promise.resolve({})) // created
.mockImplementationOnce(() => Promise.resolve(mockedData)) // Geturl
.mockImplementationOnce(() => Promise.resolve({})); // upload
wrapper = shallowMount(importModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.importExample('https://example.com/example.txt');
expect(store.dispatch).toHaveBeenCalledWith('api/getImport', { href: 'https://example.com/example.txt' });
});
test('addError', () => {
shallowMountWithDefaultStore();
const error = 'error';
wrapper.vm.addError(error);
expect(wrapper.vm.$data.errorList.length).toEqual(1);
expect(wrapper.vm.$data.errorList[0]).toEqual(error);
});
test('validateRequiredFields', () => {
shallowMountWithDefaultStore();
const question = {
question1: '1st question'
};
const arrayMapping = [
{ required: true, xlsFieldname: 'question' },
];
const keepProcessing = true;
const index = 1;
const result = wrapper.vm.validateRequiredFields(wrapper.vm, question, arrayMapping, keepProcessing, index);
expect(result).toBe(true);
});
test('validateRequiredFields question1 missing', () => {
shallowMountWithDefaultStore();
const question = {};
const arrayMapping = [
{ required: true, xlsFieldname: 'question' },
];
const keepProcessing = true;
const index = 1;
const result = wrapper.vm.validateRequiredFields(wrapper.vm, question, arrayMapping, keepProcessing, index);
expect(result).toBe(false);
});
test('parseMultivalueFields', () => {
shallowMountWithDefaultStore();
const expectedResult = {
question1: '1st question',
};
const question = {
question1: '1st question',
};
const fieldType = 'string';
const arrayMapping = [
{ required: true, xlsFieldname: 'question', type: 'string', esFieldname: 'question1' },
];
const dstField = [];
wrapper.vm.parseMultivalueFields(question, fieldType, arrayMapping, dstField);
expect(question.question1).not.toBeDefined();
expect(dstField.length).toEqual(1);
expect(dstField[0]).toEqual(expectedResult);
});
test('parseMultivalueFields -- 2 questions', () => {
shallowMountWithDefaultStore();
const expectedResult1 = {
question1: '1st question',
question2: 'default value',
};
const expectedResult2 = {
question1: '2nd question',
question2: 'default value',
}
const question = {
question1: '1st question',
question2: '2nd question',
};
const fieldType = 'string';
const arrayMapping = [
{ required: true, xlsFieldname: 'question', type: 'string', esFieldname: 'question1' },
{ required: true, xlsFieldname: 'question', type: 'boolean', esFieldname: 'question2', default: 'default value' },
];
const dstField = [];
wrapper.vm.parseMultivalueFields(question, fieldType, arrayMapping, dstField);
expect(question.question1).not.toBeDefined();
expect(question.question2).not.toBeDefined();
// While this test passes, the results don't add up.
// Consider investigating later.
expect(dstField.length).toEqual(2);
expect(dstField[0]).toEqual(expectedResult1);
expect(dstField[1]).toEqual(expectedResult2);
});
test('questionHasErrors', () => {
shallowMountWithDefaultStore();
const noQidQuestion = {
type: 'qna',
};
const noSpaceQidQuestion = {
qid: 'No Spaces.001',
q: 'Can qids have spaces?',
a: 'No',
type: 'qna',
};
const noQuestionQuestion = {
qid: '1',
q: '',
type: 'qna',
};
const noAnswerQuestion = {
qid: '1',
q: 'I have a question',
a: '',
type: 'qna',
};
const validQuestion = {
qid: '1',
q: 'I have a question',
a: 'You have an answer',
type: 'qna',
};
const notQnaTypeQuestion = {
qid: '1',
type: 'slottype',
};
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, noQidQuestion, 1)).toBe(true);
expect(wrapper.vm.$data.errorList.length).toEqual(1);
expect(wrapper.vm.$data.errorList[0]).toEqual(
`Error: No QID found for question number: ${1}. The JSON file will not be imported. Please fix and import the file again.`
);
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, noSpaceQidQuestion, 1)).toBe(true);
expect(wrapper.vm.$data.errorList.length).toEqual(2);
expect(wrapper.vm.$data.errorList[1]).toEqual(
`Error: QID: "${noSpaceQidQuestion.qid}", found for question number: ${1} must have no spaces. The JSON file will not be imported. Please fix and import the file again.`
);
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, noQuestionQuestion, 1)).toBe(true);
expect(wrapper.vm.$data.errorList.length).toEqual(3);
expect(wrapper.vm.$data.errorList[2]).toEqual(
`Error: No questions found for QID: "${noQuestionQuestion.qid}". The JSON file will not be imported. Please fix and import the file again.`
);
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, noAnswerQuestion, 1)).toBe(true);
expect(wrapper.vm.$data.errorList.length).toEqual(4);
expect(wrapper.vm.$data.errorList[3]).toEqual(
`Error: No answer found for QID: "${noAnswerQuestion.qid}". Make sure that it also includes valid characters (/[^a-zA-Z0-9-_]/g). The JSON file will not be imported. Please fix and import the file again.`
);
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, validQuestion, 1)).toBe(false);
expect(wrapper.vm.$data.errorList.length).toEqual(4);
expect(wrapper.vm.questionHasErrorsJSON(wrapper.vm, notQnaTypeQuestion, 1)).toBe(false);
expect(wrapper.vm.$data.errorList.length).toEqual(4);
});
test('extractQuestion', () => {
shallowMountWithDefaultStore();
const question = {
'question1': '1st question'
};
const result = wrapper.vm.extractQuestion(question);
expect(result.length).toEqual(1);
expect(result).toEqual(['1st question']);
});
test('processRow -- question has error(s)', () => {
shallowMountWithDefaultStore();
const question = {
// qid: '1',
q: 'I have a question',
a: 'You have an answer',
miscProperty: 'some misc value',
};
const headerMapping = {
qid: 'qid',
question: 'q',
answer: 'a',
miscProperty: 'misc',
};
const excelRowNumber = 1;
wrapper.vm.processRow(wrapper.vm, question, excelRowNumber, headerMapping);
expect(consoleLogSpy).toHaveBeenCalledWith(`Aborting load of question: ${JSON.stringify(question)}`);
});
test('processRow', () => {
shallowMountWithDefaultStore();
const question = {
qid: '1',
q: 'I have a question',
a: 'You have an answer',
miscProperty: 'some misc value',
cardtitle: 'test title',
imageurl: 'https://example.com/test.jpg',
cardsubtitle: 'test subtitle',
displaytext1: 'Press this button',
buttonvalue1: 'Button value',
'period.separated.key': 'dash-separated-value',
};
const headerMapping = {
qid: 'qid',
question: 'q',
answer: 'a',
miscProperty: 'misc',
};
const excelRowNumber = 1;
wrapper.vm.processRow(wrapper.vm, question, excelRowNumber, headerMapping);
expect(consoleLogSpy).toHaveBeenCalledWith('Processing Session Attributes');
expect(question.r).toBeDefined();
expect(question.r.title).toBeDefined();
expect(question.r.title).toEqual('test title');
expect(question.r.imageUrl).toBeDefined();
expect(question.r.imageUrl).toEqual('https://example.com/test.jpg');
expect(question.r.subTitle).toBeDefined();
expect(question.r.subTitle).toEqual('test subtitle');
expect(question.cardtitle).not.toBeDefined();
expect(question.imageurl).not.toBeDefined();
expect(question.cardsubtitle).not.toBeDefined();
expect(question.r.buttons).toBeDefined();
expect(question.r.buttons.length).toEqual(1);
expect(question.r.buttons[0].value).toEqual('Button value');
expect(question.period.separated.key).toBeDefined();
expect(question.period.separated.key).toEqual('dash-separated-value');
expect(question['period.separated.key']).not.toBeDefined();
});
});
================================================
FILE: source/website/__tests__/components/kendraIndex.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import kendraIndexModule from '../../js/components/kendraIndex.vue';
import { shallowMount } from '@vue/test-utils';
describe('kendraIndex component', () => {
let store;
let consoleLogSpy;
const shallowMountWithDefaultStore = () => {
store.dispatch.mockImplementation((eventName) => {
switch (eventName) {
case 'api/getKendraIndexingStatus':
return {
Error: '',
DashboardUrl: 'https://test.com',
Status: 'SUCCESS',
History: '',
};
case 'api/listSettings':
return [
{},
{},
{
ENABLE_KENDRA_WEB_INDEXER: 'true',
KENDRA_INDEXER_URLS: 'https://test.com',
KENDRA_WEB_PAGE_INDEX: '1',
},
];
case 'api/startKendraV2Indexing':
return Promise.resolve({});
default:
return {};
}
});
const wrapper = shallowMount(kendraIndexModule, {
global: {
mocks: {
$store: store,
},
},
});
return wrapper;
};
beforeEach(() => {
jest.resetAllMocks();
consoleLogSpy = jest.spyOn(console, "log")
consoleLogSpy.mockImplementation(jest.fn());
jest.spyOn(global, 'setTimeout').mockImplementation((fn) => {
if (typeof fn === 'function') {
fn();
}
});
store = {
dispatch: jest.fn(),
};
});
test('mounted', () => {
const wrapper = shallowMountWithDefaultStore();
expect(wrapper.exists()).toBe(true);
});
test('start', async () => {
const wrapper = shallowMountWithDefaultStore();
await wrapper.vm.start();
expect(wrapper.vm.$data.status).toBe('SUCCESS');
});
});
================================================
FILE: source/website/__tests__/components/settings.spec.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import settingsModule from '../../js/components/settings.vue';
import { shallowMount } from '@vue/test-utils';
describe('settings module', () => {
let consoleLogSpy;
let wrapper;
let store;
const shallowMountWithDefaultStore = () => {
const settings = [
{}, // default settings
{}, // custom settings
{}, // settings holder
];
store.dispatch.mockReturnValue(settings);
wrapper = shallowMount(settingsModule, {
global: {
mocks: {
$store: store
}
}
});
};
beforeEach(() => {
jest.resetAllMocks();
consoleLogSpy = jest.spyOn(console, "log")
consoleLogSpy.mockImplementation(jest.fn());
store = {
dispatch: jest.fn(),
};
});
test('_get_custom_settings', () => {
shallowMountWithDefaultStore();
wrapper.vm.$data.defaultSettings = {
key2: 'value2',
key3: 'value3',
};
wrapper.vm.$data.settingsHolder = {
key1: 'value1',
key2: 'a different value',
}
wrapper.vm.$data.customSettings = {
key3: 'value3',
};
wrapper.vm._get_custom_settings();
expect(wrapper.vm.$data.customSettings.key1).toBeDefined();
expect(wrapper.vm.$data.customSettings.key1).toEqual('value1');
expect(wrapper.vm.$data.customSettings.key2).toBeDefined();
expect(wrapper.vm.$data.customSettings.key2).toEqual('a different value');
expect(wrapper.vm.$data.customSettings.key3).not.toBeDefined();
});
test('SaveSettings', async () => {
const settings = [
{}, // default settings
{}, // custom settings
{}, // settings holder
];
store.dispatch
.mockReturnValueOnce(settings)
.mockReturnValueOnce(false);
wrapper = shallowMount(settingsModule, {
global: {
mocks: {
$store: store
}
}
});
wrapper.vm.$data.showAlert = false;
await wrapper.vm.SaveSettings();
expect(wrapper.vm.$data.showAlert).toEqual(true);
});
test('showModal / closeModal', async () => {
shallowMountWithDefaultStore();
wrapper.vm.showModal();
expect(wrapper.vm.$data.showAddModal).toEqual(true);
wrapper.vm.closeModal();
expect(wrapper.vm.$data.showAddModal).toEqual(false);
});
test('addSetting', async () => {
shallowMountWithDefaultStore();
const newValue = 'new value';
const newKey = 'newKey';
wrapper.vm.$data.newKey = newKey;
wrapper.vm.$data.newValue = newValue;
expect(wrapper.vm.$data.mergedSettings.newKey).not.toBeDefined();
expect(wrapper.vm.$data.customSettings.newKey).not.toBeDefined();
expect(wrapper.vm.$data.settingsHolder.newKey).not.toBeDefined();
wrapper.vm.addSetting();
expect(wrapper.vm.$data.mergedSettings.newKey).toBeDefined();
expect(wrapper.vm.$data.customSettings.newKey).toBeDefined();
expect(wrapper.vm.$data.settingsHolder.newKey).toBeDefined();
expect(wrapper.vm.$data.mergedSettings.newKey).toEqual(newValue);
expect(wrapper.vm.$data.customSettings.newKey).toEqual(newValue);
expect(wrapper.vm.$data.settingsHolder.newKey).toEqual(newValue);
expect(wrapper.vm.$data.newKey).toEqual('');
expect(wrapper.vm.$data.newValue).toEqual('');
});
test('resetToDefaults', async () => {
const windowSpy = jest.spyOn(window, 'window', 'get');
windowSpy.mockImplementation(() => ({
scrollTo: jest.fn(),
}));
const settings = [
{}, // default settings
{}, // custom settings
{}, // settings holder
];
store.dispatch
.mockReturnValueOnce(settings)
.mockReturnValueOnce(false);
wrapper = shallowMount(settingsModule, {
global: {
mocks: {
$store: store
}
}
});
await wrapper.vm.resetToDefaults();
expect(wrapper.vm.$data.customSettings).toEqual({});
});
});
================================================
FILE: source/website/__tests__/lib/client-auth.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const axios = require('axios');
const jwt = require('jsonwebtoken');
const queryString = require('query-string');
const clientAuth = require('../../js/lib/client-auth');
const awsMock = require('aws-sdk-client-mock');
const { fromCognitoIdentityPool } = require('@aws-sdk/credential-providers');
const { CognitoIdentityClient, GetCredentialsForIdentityCommand } = require('@aws-sdk/client-cognito-identity');
const CognitoClientMock = awsMock.mockClient(CognitoIdentityClient);
jest.mock('axios');
jest.mock('@aws-sdk/credential-providers');
jest.mock('@aws-sdk/client-cognito-identity');
jest.mock('@aws-sdk/client-lex-runtime-service');
jest.mock('@aws-sdk/client-lex-runtime-v2');
jest.mock('@aws-sdk/client-polly');
jest.mock('jsonwebtoken');
jest.mock('query-string');
describe('clientAuth', () => {
let windowMock;
beforeEach(() => {
CognitoClientMock.reset();
jest.clearAllMocks();
windowMock = {
alert: jest.fn(),
confirm: jest.fn(),
sessionStorage: {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
},
location: {
href: 'http://localhost/',
origin: 'http://localhost',
pathname: '/',
search: '?code=123456',
},
};
global.window = windowMock;
queryString.parse.mockReturnValue({ code: '123456' });
queryString.stringify.mockImplementation((obj) => {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(obj)) {
params.append(key, value);
}
return params.toString();
});
});
afterEach(() => {
global.window = global.window;
});
it('should fetch tokens and credentials when code is present', async () => {
const mockHeadResponse = { headers: { 'api-stage': 'test' } };
const mockGetResponse = {
data: {
region: 'us-east-1',
UserPool: 'pool-id',
PoolId: 'pool-id',
ClientIdClient: 'client-id',
_links: {
CognitoEndpoint: { href: 'https://cognito-endpoint.com' },
ClientLogin: { href: 'https://login.com' },
},
},
};
const mockTokenResponse = {
data: {
id_token: 'id-token',
refresh_token: 'refresh-token',
},
};
const testToken = {
'cognito:username': 'test-user',
'cognito:groups': 'testgroup',
};
axios.head.mockResolvedValue(mockHeadResponse);
axios.get.mockResolvedValue(mockGetResponse);
axios.mockResolvedValue(mockTokenResponse);
jwt.decode.mockReturnValue(testToken);
CognitoClientMock.on(GetCredentialsForIdentityCommand).resolves({
Credentials: {
accessKeyId: 'accessKeyId',
identityId: 'identityId',
secretAccessKey: 'secretAccessKey',
sessionToken: 'sessionToken',
expiration: 'expiration',
},
});
const result = await clientAuth().catch(() => {});
result.username = testToken['cognito:username'];
expect(axios.head).toHaveBeenCalledWith('http://localhost/');
expect(axios.get).toHaveBeenCalledWith('/test');
expect(axios).toHaveBeenCalledWith(
{"data": "grant_type=authorization_code&client_id=client-id&code=123456&redirect_uri=http%3A%2F%2Flocalhost%2F", "headers": {"Content-Type": "application/x-www-form-urlencoded"}, "method": "POST", "url": "https://cognito-endpoint.com/oauth2/token"}
);
expect(jwt.decode).toHaveBeenCalledWith('id-token');
expect(result).toEqual({
config: {
region: 'us-east-1',
credentials: expect.any(Object),
},
lexV1: expect.any(Object),
lexV2: expect.any(Object),
polly: expect.any(Object),
username: 'test-user',
Login: 'https://login.com',
idtoken: 'id-token',
});
});
it('should refresh tokens when refresh token is present', async () => {
const mockHeadResponse = { headers: { 'api-stage': 'test' } };
const mockGetResponse = {
data: {
region: 'us-east-1',
UserPool: 'pool-id',
PoolId: 'pool-id',
ClientIdClient: 'client-id',
_links: {
CognitoEndpoint: { href: 'https://cognito-endpoint.com' },
ClientLogin: { href: 'https://login.com' },
},
},
};
const mockTokenResponse = {
data: {
id_token: 'new-id-token',
},
};
windowMock.sessionStorage.getItem.mockReturnValue('refresh-token');
queryString.parse.mockReturnValue({ code: '123456' });
axios.head.mockResolvedValue(mockHeadResponse);
axios.get.mockResolvedValue(mockGetResponse);
axios.mockResolvedValue(mockTokenResponse);
CognitoClientMock.on(GetCredentialsForIdentityCommand).resolves({
Credentials: {
accessKeyId: 'accessKeyId',
identityId: 'identityId',
secretAccessKey: 'secretAccessKey',
sessionToken: 'sessionToken',
expiration: 'expiration',
},
});
const result = await clientAuth().catch(() => {});
expect(axios.head).toHaveBeenCalledWith('http://localhost/');
expect(axios.get).toHaveBeenCalledWith('/test');
expect(axios).toHaveBeenCalledWith(
{"data": "grant_type=refresh_token&client_id=client-id&refresh_token=refresh-token", "headers": {"Content-Type": "application/x-www-form-urlencoded"}, "method": "POST", "url": "https://cognito-endpoint.com/oauth2/token"}
);
const testToken = {
'cognito:username': 'test-user',
'cognito:groups': 'testgroup',
};
result.username = testToken['cognito:username'];
expect(result).toEqual({
config: {
region: 'us-east-1',
credentials: expect.any(Object),
},
lexV1: expect.any(Object),
lexV2: expect.any(Object),
polly: expect.any(Object),
username: 'test-user',
Login: 'https://login.com',
idtoken: 'new-id-token',
});
});
it('should use Cognito identity credentials when no code is present', async () => {
windowMock.location.href = 'http://localhost/';
windowMock.location.search = '';
queryString.parse.mockReturnValue({});
const mockHeadResponse = { headers: { 'api-stage': 'test' } };
const mockGetResponse = {
data: {
region: 'us-east-1',
UserPool: 'pool-id',
PoolId: 'pool-id',
ClientIdClient: 'client-id',
_links: {
CognitoEndpoint: { href: 'https://cognito-endpoint.com' },
ClientLogin: { href: 'https://login.com' },
},
},
};
axios.head.mockResolvedValue(mockHeadResponse);
axios.get.mockResolvedValue(mockGetResponse);
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce({}));
const result = await clientAuth().catch(() => {});
expect(axios.head).toHaveBeenCalledWith('http://localhost/');
expect(axios.get).toHaveBeenCalledWith('/test');
expect(axios.post).not.toHaveBeenCalled();
expect(windowMock.sessionStorage.getItem).not.toHaveBeenCalled();
expect(result).toEqual({
config: {
region: 'us-east-1',
credentials: expect.any(Object),
},
lexV1: expect.any(Object),
lexV2: expect.any(Object),
polly: expect.any(Object),
username: undefined,
Login: 'https://login.com',
idtoken: undefined,
});
});
});
================================================
FILE: source/website/__tests__/lib/index.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const indexModule = require('../../js/lib/index');
jest.mock('../../js/lib/router', () => {});
jest.mock('../../js/lib/store', () => {});
describe('lib js index module', () => {
test('it exists', () => {
expect(indexModule).toBeDefined();
});
});
================================================
FILE: source/website/__tests__/lib/router.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import routerModule from '../../js/lib/router';
jest.mock('vue-router', () => ({
createWebHistory: jest.fn(),
createWebHashHistory: jest.fn(),
}));
const defaultModuleMock = () => ({
default: {},
});
jest.mock('../../js/components/hooks/index.vue', () => defaultModuleMock());
describe('js lib router module', () => {
test('routes exist', () => {
expect(routerModule.routes).toBeDefined();
expect(routerModule.routes.length).toBeGreaterThan(0);
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/connect.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from './mockedContext';
const connect = require('../../../../../js/lib/store/api/actions/connect');
const contactFlow = {
CallFlow: '',
FileName: '',
QnaFile: '',
};
describe('connect action test', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('dispatch', () => {
mockedContext.dispatch.mockReturnValueOnce(contactFlow);
const result = connect.getContactFlow(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.connect.href,
method: 'get',
});
expect(result).toEqual(contactFlow);
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/export.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from './mockedContext';
const exportModule = require('../../../../../js/lib/store/api/actions/export');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
describe('export action test', () => {
const mockedInfo = {
_links: {
exports: {
href: '',
},
imports: {
href: '',
},
},
};
beforeEach(() => {
jest.resetAllMocks();
s3ClientMock.reset();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('startKendraSyncExport with filter', async () => {
const opts = {
name: 'test-name',
filter: 'filter',
};
mockedContext.dispatch.mockReturnValueOnce(mockedInfo);
await exportModule.startKendraSyncExport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedInfo._links.exports.href}/${opts.name}`,
method: 'put',
body: {
filter: `${opts.filter}.*`,
prefix: 'kendra-',
},
});
});
test('startKendraSyncExport without filter', async () => {
const opts = {
name: 'test-name',
};
mockedContext.dispatch.mockReturnValueOnce(mockedInfo);
await exportModule.startKendraSyncExport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedInfo._links.exports.href}/${opts.name}`,
method: 'put',
body: {
prefix: 'kendra-',
},
});
});
test('downloadExport', async () => {
const opts = {
bucket: 'test-bucket'
};
const bodyString = 'test';
const expectedResult = `{"qna":[${bodyString}]}`;
s3ClientMock.on(GetObjectCommand).resolvesOnce({
Body: {
transformToString: () => bodyString
},
});
const downloadExport = exportModule.downloadExport;
const result = await downloadExport(mockedContext, opts);
expect(result).toEqual(expectedResult);
});
test('waitForExport finds id', async () => {
const opts = { id: 'test-id' };
const mockedResults = {
jobs: [
{ id: 'test-id' },
{ id: 'not-the-test-id-you-want' },
],
};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
// Mock the returned values from the dispatch method call in chronological order.
mockedContext.dispatch
.mockReturnValueOnce(mockedInfo)
.mockReturnValueOnce(mockedResults);
await exportModule.waitForExport(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledTimes(0);
});
test('waitForExport does not find id', async () => {
const opts = { id: 'not-a-test-id' };
const mockedResults = {
jobs: [
{ id: 'test-id' },
{ id: 'not-the-test-id-you-want' },
],
};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
// Mock the returned values from the dispatch method call in chronological order.
mockedContext.dispatch
.mockReturnValueOnce(mockedInfo)
.mockReturnValueOnce(mockedResults);
await exportModule.waitForExport(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
});
test('waitForExport throws an error', async () => {
const opts = {};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
// Force the dispatch method to throw an error.
mockedContext.dispatch.mockImplementationOnce(() => {
throw new Error('test error -- a valid error');
});
await exportModule.waitForExport(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
});
test('listExports', async () => {
const opts = {};
const listOfExports = 'list of exports';
mockedContext.dispatch
.mockReturnValueOnce(mockedInfo)
.mockReturnValueOnce(listOfExports);
const result = await exportModule.listExports(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedInfo._links.exports.href,
method: 'get',
});
expect(result).toEqual(listOfExports);
});
test('getExport', async () => {
const opts = { href: '' };
await exportModule.getExport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'get',
});
});
test('getExportByJobId', async () => {
const id = 'test-id';
await exportModule.getExportByJobId(mockedContext, id);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.jobs.href}/exports/${id}`,
method: 'get',
});
});
test('deleteExport', async () => {
const opts = { href: '' };
await exportModule.deleteExport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'delete',
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/genesys.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from './mockedContext';
const genesysModule = require('../../../../../js/lib/store/api/actions/genesys');
describe('genesys action test', () => {
test('getGenesysCallFlow is called.', () => {
const opts = {};
genesysModule.getGenesysCallFlow(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.genesys.href,
method: 'get',
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/import.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from './mockedContext';
const importModule = require('../../../../../js/lib/store/api/actions/import');
const { S3Client } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
describe('import action test', () => {
beforeEach(() => {
jest.resetAllMocks();
s3ClientMock.reset();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('listExamples', async () => {
const mockedResponse = {
examples: [
{
id: 'example-1',
description: {},
},
{
id: 'example-2',
description: {},
},
],
};
mockedContext.dispatch.mockReturnValueOnce(mockedResponse);
await importModule.listExamples(mockedContext);
expect(mockedContext.dispatch).toBeCalledTimes(1);
});
test('listExamples with href to dispatch', async () => {
const mockedResponse = {
examples: [
{
id: 'example-1',
description: {
href: 'https://example.com',
},
},
{
id: 'example-2',
description: {
href: 'https://example2.com',
},
},
],
};
mockedContext.dispatch.mockReturnValueOnce(mockedResponse);
await importModule.listExamples(mockedContext);
expect(mockedContext.dispatch).toBeCalledTimes(1 + mockedResponse.examples.length);
});
test('getExampleDescription', async () => {
const mockedExample = {
description: {
href: 'https://example.com',
},
};
await importModule.getExampleDescription(mockedContext, mockedExample);
expect(mockedContext.dispatch).toBeCalledWith('_request', {
url: mockedExample.description.href,
method: 'get',
});
});
test('startImport', async () => {
const mockedOpts = {
name: 'test-name',
qa: [
'item1',
'item2',
],
};
const mockedResponse = {
_links: {
imports: {
bucket: 'test-bucket',
uploadPrefix: 'test-prefix',
},
},
};
mockedContext.dispatch.mockReturnValueOnce(mockedResponse);
s3ClientMock.send.returns('success');
const result = await importModule.startImport(mockedContext, mockedOpts);
expect(result).toEqual('success');
});
test('waitForImport with job found', async () => {
const mockedResponse = {
_links: {
imports: {
href: '',
},
},
};
const mockedResults = {
jobs: [
{ id: 'test-id' },
{ id: 'not-the-test-id-you-want' },
],
};
const opts = {
id: 'test-id',
};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
mockedContext.dispatch
.mockReturnValueOnce(mockedResponse)
.mockReturnValueOnce(mockedResults);
await importModule.waitForImport(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledTimes(0);
});
test('waitForImport with job not found', async () => {
const mockedResponse = {
_links: {
imports: {
href: '',
},
},
};
const mockedResults = {
jobs: [
{ id: 'test-id' },
{ id: 'not-the-test-id-you-want' },
],
};
const opts = {
id: 'test-id-not-in-the-job-list',
};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
mockedContext.dispatch
.mockReturnValueOnce(mockedResponse)
.mockReturnValueOnce(mockedResults);
await importModule.waitForImport(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
});
test('waitForImport with error thrown', async () => {
const mockedResponse = {
_links: {
imports: {
href: '',
},
},
};
const mockedResults = {
jobs: [
{ id: 'test-id' },
{ id: 'not-the-test-id-you-want' },
],
};
const opts = {
id: 'test-id-not-in-the-job-list',
};
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn().mockImplementation((error) => error);
const mockedError = new Error('test error -- a valid error');
mockedContext.dispatch.mockImplementationOnce(() => {
throw mockedError;
});
await importModule.waitForImport(mockedContext, opts).then(resFunction, rejFunction);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toBeCalledWith(mockedError);
});
test('listImports', async () => {
const mockedResponse = {
_links: {
imports: {
href: '',
},
},
};
mockedContext.dispatch.mockReturnValueOnce(mockedResponse);
await importModule.listImports(mockedContext, {});
expect(mockedContext.dispatch).toBeCalledTimes(2);
});
test('getImport', () => {
const opts = {
href: '',
};
importModule.getImport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'get',
});
});
test('deleteImport', () => {
const opts = {
href: '',
};
importModule.deleteImport(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'delete',
});
});
test('getTerminologies', () => {
const opts = {
href: '',
};
importModule.getTerminologies(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.translate.href}/list`,
method: 'post',
});
});
test('startImportTranslate', () => {
const opts = {
name: '',
description: '',
file: '',
};
importModule.startImportTranslate(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.translate.href}/import`,
method: 'post',
body:
{
name: opts.name,
description: opts.description,
file: opts.file,
},
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/index.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import exp from 'constants';
import mockedContext from './mockedContext';
const query = require('query-string').stringify;
const indexModule = require('../../../../../js/lib/store/api/actions/index');
const axios = require('axios');
const { sign } = require('aws4');
jest.mock('axios');
jest.mock('aws4');
describe('index action test', () => {
class CustomError extends Error {
constructor(message, response, code) {
super(message);
this.name = 'CustomError';
this.response = response;
this.code = code;
}
}
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('_request', async () => {
const opts = {
url: 'https://example.com',
method: 'GET',
headers: {},
body: 'Test',
};
const successResult = {
data: 'success',
};
axios.mockResolvedValueOnce(successResult);
const result = await indexModule._request(mockedContext, opts);
expect(result).toEqual(successResult.data);
});
test('_request with http 404 response error', async () => {
const opts = {
url: 'https://example.com',
method: 'GET',
headers: {},
body: 'Test',
ignore404: true,
};
const customError = new CustomError('Test', { status: 404 });
axios.mockImplementationOnce(() => {
throw customError;
});
const resFunction = jest.fn();
const rejFunction = jest.fn((error) => error);
await indexModule._request(mockedContext, opts).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledWith(new Error('does-not-exist'));
});
test('botinfo', () => {
indexModule.botinfo(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.bot.href,
method: 'get',
reason: 'Failed to get BotInfo',
});
});
test('alexa', () => {
indexModule.alexa(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.bot._links.alexa.href,
method: 'get',
reason: 'Failed to get Alexa info',
});
});
test('schema', () => {
indexModule.schema(mockedContext, {});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.questions.href,
method: 'options',
reason: 'Failed to get qa options',
});
});
test('list with filter', () => {
const opts = {
perpage: 50,
page: 1,
filter: 'test filter',
order: 'descending',
};
indexModule.list(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.questions.href}?${query({
from: opts.page * opts.perpage,
filter: `${opts.filter}.*`,
order: opts.order,
perpage: opts.perpage,
})}`,
method: 'get',
reason: `Failed to get page:${opts.page}`,
});
});
test('list without filter', () => {
const opts = {
perpage: 50,
page: 1,
filter: '',
order: 'descending',
};
indexModule.list(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.questions.href}?${query({
from: opts.page * opts.perpage,
filter: '',
order: opts.order,
perpage: opts.perpage,
})}`,
method: 'get',
reason: `Failed to get page:${opts.page}`,
});
});
test('check finds qid', async () => {
const resFunction = jest.fn((result) => result);
const rejFunction = jest.fn();
const qid = 'test-qid';
await indexModule.check(mockedContext, qid).then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledTimes(0);
expect(resFunction).toHaveBeenCalledWith(true);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.questions.href}/${encodeURIComponent(qid)}`,
method: 'head',
reason: `${qid} does not exists`,
ignore404: true,
});
});
test('check encounters does-not-exist error', async () => {
const resFunction = jest.fn();
const rejFunction = jest.fn();
mockedContext.dispatch.mockImplementationOnce(() => {
throw Error('does-not-exist');
});
await indexModule.check(mockedContext, 'test-qid').then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledTimes(0);
});
test('check encounters any other error', async () => {
const resFunction = jest.fn();
const rejFunction = jest.fn();
mockedContext.dispatch.mockImplementationOnce(() => {
throw new Error('Some unknown test error message');
});
await indexModule.check(mockedContext, 'test-qid').then(resFunction, rejFunction);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
});
test('add', () => {
indexModule.add(mockedContext, {});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('update', {});
});
test('update', () => {
const payload = { qid: 'test-qid' };
indexModule.update(mockedContext, payload);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.questions.href}/${encodeURIComponent(payload.qid)}`,
method: 'put',
body: payload,
reason: 'failed to update',
});
});
test('remove', () => {
const qid = 'test-id';
indexModule.remove(mockedContext, qid);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${mockedContext.rootState.info._links.questions.href}/${encodeURIComponent(qid)}`,
method: 'delete',
reason: 'failed to delete',
});
});
test('removeBulk', () => {
const list = ['test-qid-1', 'test-qid-2'];
indexModule.removeBulk(mockedContext, list);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.questions.href,
method: 'delete',
reason: 'failed to delete',
body: { list },
});
});
test('removeQuery', () => {
const mockedQuery = { query: '' };
indexModule.removeQuery(mockedContext, mockedQuery);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.questions.href,
method: 'delete',
reason: 'failed to delete',
body: { query: mockedQuery },
});
});
test('build', () => {
indexModule.build(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.bot.href,
method: 'post',
body: {},
reason: 'failed to build',
});
});
test('status', () => {
indexModule.status(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.bot.href,
method: 'get',
reason: 'failed to get status',
});
});
test('search with defaults', () => {
const opts = {
query: {
testAttribute: 'testValue',
},
topic: '',
client_filter: '',
score_on: '',
from: 0,
};
const url = `${mockedContext.rootState.info._links.questions.href}?${query({
query: opts.query,
topic: '',
client_filter: '',
score_answer: 'false',
score_text_passage: 'false',
from: 0,
})}`;
indexModule.search(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: url,
method: 'get',
reason: 'failed to get search',
});
});
test('search with non-defaults 1', () => {
const opts = {
query: {
testAttribute: 'testValue',
},
topic: 'test-topic',
client_filter: 'test client_filter',
score_on: 'qna item answer',
from: 2,
};
const url = `${mockedContext.rootState.info._links.questions.href}?${query({
query: opts.query,
topic: opts.topic,
client_filter: opts.client_filter,
score_answer: 'true',
score_text_passage: 'false',
from: opts.from,
})}`;
indexModule.search(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: url,
method: 'get',
reason: 'failed to get search',
});
});
test('search with non-defaults 2', () => {
const opts = {
query: {
testAttribute: 'testValue',
},
topic: 'test-topic',
client_filter: 'test client_filter',
score_on: 'text item passage',
from: 2,
};
const url = `${mockedContext.rootState.info._links.questions.href}?${query({
query: opts.query,
topic: opts.topic,
client_filter: opts.client_filter,
score_answer: 'false',
score_text_passage: 'true',
from: opts.from,
})}`;
indexModule.search(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: url,
method: 'get',
reason: 'failed to get search',
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/kendraIndex.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from "./mockedContext";
const kendraIndexModule = require('../../../../../js/lib/store/api/actions/kendraIndex');
describe('kendraIndex action test', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('startKendraV2Indexing', () => {
kendraIndexModule.startKendraV2Indexing(mockedContext, {});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.crawlerV2.href,
method: 'post',
});
});
test('getKendraIndexingStatus', () => {
kendraIndexModule.getKendraIndexingStatus(mockedContext, {});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.crawlerV2.href,
method: 'get',
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/mockedContext.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mockedContext = {
commit: jest.fn(),
dispatch: jest.fn(),
rootState: {
info: {
_links: {
connect: {
href: '',
},
jobs: {
href: '',
},
genesys: {
href: '',
},
examples: {
href: '',
},
translate: {
href: '',
},
bot: {
href: '',
},
alexa: {
href: '',
},
questions: {
href: '',
},
crawlerV2: {
href: '',
},
crawler: {
href: '',
},
},
SettingsTable: '',
Version: '1.0.0',
region: 'us-weast',
},
user: {
credentials: 'test-credentials',
},
bot: {
_links: {
alexa: {
href: '',
},
},
},
},
};
export default mockedContext;
================================================
FILE: source/website/__tests__/lib/store/api/actions/settings.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import settingsModule from '../../../../../js/lib/store/api/actions/settings';
const awsMock = require('aws-sdk-client-mock');
const { DynamoDBClient, DeleteItemCommand, GetItemCommand, PutItemCommand, ScanCommand, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
const dynamodbMock = awsMock.mockClient(DynamoDBClient);
describe('settings action', () => {
beforeEach(() => {
dynamodbMock.reset();
});
test('listSettings', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: 'mockedValue',
DefaultQnABotSettings: 'mockedValue',
PrivateQnABotSettings: 'mockedValue',
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(ScanCommand).resolves({ Items: [{"SettingCategory":{"S":"Advanced"},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"ES_PHRASE_BOOST"}}] })
const result = await settingsModule.listSettings(mockedContext);
expect(result).toEqual([{ES_PHRASE_BOOST:4}, {}, {ES_PHRASE_BOOST:4}]);
});
test('listSettings -- sentinel value converts to empty string', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: 'mockedValue',
DefaultQnABotSettings: 'mockedValue',
PrivateQnABotSettings: 'mockedValue',
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(ScanCommand).resolves({ Items: [
{"SettingCategory":{"S":"BedrockRag"},"nonce":{"N":"1"},"DefaultValue":{"S":"From Knowledge Base:"},"SettingName":{"S":"KNOWLEDGE_BASE_PREFIX_MESSAGE"},"SettingValue":{"S":"EMPTY_STRING_BY_USER"}}
] });
const result = await settingsModule.listSettings(mockedContext);
// Sentinel should be converted to empty string in both custom and merged
expect(result).toEqual([
{KNOWLEDGE_BASE_PREFIX_MESSAGE: "From Knowledge Base:"},
{KNOWLEDGE_BASE_PREFIX_MESSAGE: ""},
{KNOWLEDGE_BASE_PREFIX_MESSAGE: ""}
]);
});
test('listSettings -- error thrown', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: {},
DefaultQnABotSettings: {},
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(ScanCommand).rejects('mocked rejection');
await expect(settingsModule.listSettings(mockedContext)).rejects.toThrow('mocked rejection');
});
test('Update and Restore Settings', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: {},
DefaultQnABotSettings: {},
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(GetItemCommand).resolves({ Item: {"SettingCategory":{"S":"Advanced"},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"ES_PHRASE_BOOST"}} })
dynamodbMock.on(UpdateItemCommand).resolves({ Attributes: { SettingName: { S: 'ES_PHRASE_BOOST' }, SettingValue: { N: 3 }, SettingCategory: { S: 'Advanced' }, DefaultValue: { N: '4' }, nonce: { N: '0' } } })
// GetParameters shows that there's one setting left out of the new settings JSON that was just saved
dynamodbMock.on(ScanCommand).resolves({ Items: [{"SettingCategory":{"S":"Advanced"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"ES_PHRASE_BOOST"}}, {"SettingCategory":{"S":"Advanced"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"LAMBDA_POSTPROCESS_HOOK"}}] })
dynamodbMock.on(GetItemCommand, {TableName: mockedContext.rootState.info.SettingsTable, Key: {"SettingName": {"S": "LAMBDA_POSTPROCESS_HOOK"}, "SettingCategory": {"S": "Advanced"}}}).resolves({ Item: {"SettingCategory":{"S":"Advanced"},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"ES_PHRASE_BOOST"}} })
const result = await settingsModule.updateSettings(mockedContext, { ES_PHRASE_BOOST: 3 });
expect(result).toEqual({"changedSettings":["ES_PHRASE_BOOST",],"restoredSettings":["LAMBDA_POSTPROCESS_HOOK",]});
});
test('Update and Restore Custom Settings', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: {},
DefaultQnABotSettings: {},
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(GetItemCommand, {
TableName: mockedContext.rootState.info.SettingsTable,
Key: {
SettingName: {"S":'Test1'},
SettingCategory: {"S":'Custom'},
},
}).resolves({})
dynamodbMock.on(GetItemCommand).resolves({ Item: {"SettingCategory":{"S":"Custom"},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"Test1"}} })
dynamodbMock.on(PutItemCommand).resolves({ Attributes: { SettingName: { S: 'Test1' }, SettingValue: { N: 3 }, SettingCategory: { S: 'Custom' }, DefaultValue: { N: '4' }, nonce: { N: '0' } } })
// GetParameters shows that there's one setting left out of the new settings JSON that was just saved
dynamodbMock.on(ScanCommand).resolves({ Items: [{"SettingCategory":{"S":"Custom"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"Test1"}}, {"SettingCategory":{"S":"Custom"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"Test2"}}] })
dynamodbMock.on(DeleteItemCommand, {TableName: mockedContext.rootState.info.SettingsTable, Key: {"SettingName": {"S": "Test2"}, "SettingCategory": {"S": "Custom"}}}).resolves({})
const result = await settingsModule.updateSettings(mockedContext, { Test1: 3 });
expect(result).toEqual({"changedSettings":["Test1",],"restoredSettings":["Test2",]});
});
test('listPrivateSettings', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: 'mockedValue',
DefaultQnABotSettings: 'mockedValue',
PrivateQnABotSettings: 'mockedValue',
SettingsTable: 'mockedTableName'
},
},
};
dynamodbMock.on(ScanCommand).resolves({ Items: [{"SettingCategory":{"S":"Custom"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"Test1"}}, {"SettingCategory":{"S":"Custom"}, "SettingValue": {"N":4},"nonce":{"N":"0"},"DefaultValue":{"N":"4"},"SettingName":{"S":"Test2"}}] })
const result = await settingsModule.listPrivateSettings(mockedContext);
expect(result).toEqual({"Test1":4,"Test2":4});
});
test('listPrivateSettings - error thrown', async () => {
const mockedContext = {
rootState: {
user: {
credentials: '',
},
info: {
CustomQnABotSettings: 'mockedValue',
DefaultQnABotSettings: 'mockedValue',
PrivateQnABotSettings: 'mockedValue',
},
},
};
dynamodbMock.on(ScanCommand).rejects('mocked rejection');
const result = await settingsModule.listPrivateSettings(mockedContext, {});
expect(result).toEqual({});
});
test('getSettingsMap', async () => {
const settingsMap = settingsModule.getSettingsMap();
const groupKeys = Object.keys(settingsMap);
groupKeys.forEach((groupKey) => {
if (settingsMap[groupKey].label === undefined) {
throw new Error(`${groupKey} is missing the 'label' attribute.`);
}
if (settingsMap[groupKey].openedPanels === undefined) {
throw new Error(`${groupKey} is missing the 'openedPanels' attribute.`);
}
if (settingsMap[groupKey].subgroups === undefined) {
throw new Error(`${groupKey} is missing the 'subgroups' attribute.`);
}
const memberIds = {};
const subgroupKeys = Object.keys(settingsMap[groupKey].subgroups);
subgroupKeys.forEach((subgroupKey) => {
if (settingsMap[groupKey].subgroups[subgroupKey].label === undefined) {
throw new Error(`${subgroupKey} is missing the 'label' attribute.`);
}
if (settingsMap[groupKey].subgroups[subgroupKey].members === undefined) {
throw new Error(`${subgroupKey} is missing the 'members' attribute.`);
}
if (settingsMap[groupKey].subgroups[subgroupKey].id === undefined) {
throw new Error(`${subgroupKey} is missing the 'id' attribute.`);
}
if (settingsMap[groupKey].subgroups[subgroupKey].collapsed === undefined) {
throw new Error(`${subgroupKey} is missing the 'collapsed' attribute.`);
}
settingsMap[groupKey].subgroups[subgroupKey].members.forEach((member, index) => {
if (member.id === undefined || member.id === null || member.id === '') {
throw new Error(`${subgroupKey}.member ${index} is missing the 'id' attribute.`);
}
if (member.hint === undefined || member.hint === null || member.hint === '') {
throw new Error(`${subgroupKey}.member with 'id'=${member.id} is missing the 'hint' attribute.`);
}
if (member.type) {
if (!['string', 'number', 'boolean', 'enum', 'textarea'].find((memberType) => memberType === member.type)) {
throw new Error(`${subgroupKey}.member with 'id'='${member.id}' has an invalid 'type' attribute, '${member.type}'.`);
}
}
if (member.type === 'enum' && (!member.enums || member.enums.length === 0)) {
throw new Error(`${subgroupKey}.member with 'id'='${member.id}' has missing or empty 'enums' attribute.`);
}
if (memberIds[member.id]) {
throw new Error(`${subgroupKey}.member with 'id'='${member.id}' has a duplicate.`);
} else {
memberIds[member.id] = true;
}
});
});
});
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/testall.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import mockedContext from './mockedContext';
const testallModule = require('../../../../../js/lib/store/api/actions/testall');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { mockClient } = require('aws-sdk-client-mock');
const s3ClientMock = mockClient(S3Client);
describe('testall action test', () => {
const testResponse = {
_links: {
testall: {
href: 'test-href',
},
},
};
const testInfo = {
_links: {
testall: {
href: 'test-href',
}
}
};
beforeEach(() => {
jest.resetAllMocks();
s3ClientMock.reset();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('startTestAll with filter', async () => {
const opts = {
filter: 'test-filter',
token: 'test-token',
name: 'test-name',
locale: 'es_US',
};
mockedContext.dispatch.mockReturnValueOnce(testInfo);
await testallModule.startTestAll(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${testInfo._links.testall.href}/${opts.name}`,
method: 'put',
body: {
filter: `${opts.filter}.*`,
token: `${opts.token}`,
locale: 'es_US'
},
});
});
test('startTestAll without filter', async () => {
const opts = {
filter: '',
token: 'test-token',
name: 'test-name',
locale: '',
};
mockedContext.dispatch.mockReturnValueOnce(testInfo);
await testallModule.startTestAll(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: `${testInfo._links.testall.href}/${opts.name}`,
method: 'put',
body: {
token: `${opts.token}`,
locale: 'en_US',
},
});
});
test('downloadTestAll', async () => {
const opts = {
bucket: 'test-bucket',
key: 'test-key',
};
const testResult = 'Some result from S3';
s3ClientMock.on(GetObjectCommand).resolvesOnce({
Body: {
transformToString: () => testResult,
},
});
const downloadTestAll = testallModule.downloadTestAll;
const result = await downloadTestAll(mockedContext, opts);
expect(result).toEqual(testResult);
});
test('waitForTestAll found the job', async () => {
const opts = {
id: 'the-one-you-want',
};
const jobs = [
{ id: 'the-one-you-want' },
{ id: 'not-the-one-you-want' },
];
const resFunction = jest.fn().mockImplementation((job) => job);
const rejFunction = jest.fn();
mockedContext.dispatch
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs });
await testallModule.waitForTestAll(mockedContext, opts).then(resFunction, rejFunction);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: testResponse._links.testall.href,
method: 'get',
});
expect(resFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledTimes(0);
});
test('waitForTestAll did not find the job', async () => {
const opts = {
id: 'id-that-does-not-exist',
};
const jobs = [
{ id: 'the-one-you-want' },
{ id: 'not-the-one-you-want' },
];
const resFunction = jest.fn();
const rejFunction = jest.fn((err) => err);
mockedContext.dispatch
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs })
.mockReturnValueOnce(testResponse)
.mockReturnValueOnce({ jobs });
await testallModule.waitForTestAll(mockedContext, opts).then(resFunction, rejFunction);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: testResponse._links.testall.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(22);
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledWith('timeout');
});
test('waitForTestAll threw an error', async () => {
const opts = {
id: 'id-that-does-not-exist',
};
const jobs = [
{ id: 'the-one-you-want' },
{ id: 'not-the-one-you-want' },
];
const mockedError = new Error('test-error');
const resFunction = jest.fn();
const rejFunction = jest.fn((err) => err);
mockedContext.dispatch.mockImplementationOnce(() => { throw mockedError; });
await testallModule.waitForTestAll(mockedContext, opts).then(resFunction, rejFunction);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(resFunction).toHaveBeenCalledTimes(0);
expect(rejFunction).toHaveBeenCalledTimes(1);
expect(rejFunction).toHaveBeenCalledWith(mockedError);
});
test('listTestAll', async () => {
mockedContext.dispatch.mockReturnValueOnce(testResponse);
await testallModule.listTestAll(mockedContext, {});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: mockedContext.rootState.info._links.jobs.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: testResponse._links.testall.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
});
test('getTestAll', () => {
const opts = {
href: 'test-href',
};
testallModule.getTestAll(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'get',
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
});
test('deleteTestAll', () => {
const opts = {
href: 'test-href',
};
testallModule.deleteTestAll(mockedContext, opts);
expect(mockedContext.dispatch).toHaveBeenCalledWith('_request', {
url: opts.href,
method: 'delete',
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
});
});
================================================
FILE: source/website/__tests__/lib/store/api/actions/util.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import { getUserAgentString } from "../../../../../js/capability/util";
describe("getUserAgentString", () => {
test('Returns the correct user agent string', () => {
const version = '9.9.9';
const capability = 'CTEST';
const userAgent = getUserAgentString(version, capability);
expect(userAgent).toEqual([
[`AWSSOLUTION/SO0189/v${version}`],
[`AWSSOLUTION-CAPABILITY/SO0189-${capability}/v${version}`],
]);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/actions/add.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const addModule = require('../../../../../js/lib/store/data/actions/add');
const util = require('../../../../../js/lib/store/data/actions/util');
jest.mock('../../../../../js/lib/store/data/actions/util');
describe('add data action', () => {
const testToken = 'test-token';
const mockedContext = {
commit: jest.fn(),
rootState: {
bot: {
status: 'Submitting',
build: {
message: '',
token: '',
status: '',
},
},
},
};
const mockedReadyResult = {
token: testToken,
status: 'READY',
};
const mockedBuildingResult = {
token: testToken,
status: 'BUILDING',
};
const mockedReadyInfo = {
build: {
token: testToken,
status: 'READY',
message: 'test message',
},
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('build with READY status', async () => {
const testToken = 'test-token';
util.api
.mockResolvedValueOnce(mockedReadyResult)
.mockResolvedValueOnce(mockedBuildingResult)
.mockResolvedValueOnce(mockedReadyInfo);
await addModule.build(mockedContext);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'botinfo');
expect(util.api).toHaveBeenCalledWith(mockedContext, 'build');
expect(util.api).toHaveBeenCalledWith(mockedContext, 'botinfo');
expect(util.api).toHaveBeenCalledTimes(3);
});
test('add', async () => {
const qa = 'score';
await addModule.add(mockedContext, qa);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'update', 'score');
expect(util.api).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('page/incrementTotal', null, { root: true });
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/actions/delete.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const deleteModule = require('../../../../../js/lib/store/data/actions/delete');
const util = require('../../../../../js/lib/store/data/actions//util');
jest.mock('../../../../../js/lib/store/data/actions//util');
describe('delete data action', () => {
const mockedContext = {
dispatch: jest.fn(),
commit: jest.fn(),
state: {
QAs: [
{ qid: 'the-one-you-want' },
{ qid: 'not-the-one-you-want' },
{ qid: 'remove-in-bulk' },
{ qid: 'also-remove-in-bulk' },
],
filter: 'test-filter',
},
};
const mockItem = {
questions: [
'First question',
'Second question',
'Third question',
],
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('removeQ success', async () => {
await deleteModule.removeQ(mockedContext, { index: 1, item: mockItem });
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('update', {
qa: {
questions: [
'First question',
'Third question',
],
}
});
});
test('removeQ failure', async () => {
const resFunction = jest.fn();
const rejFunction = jest.fn().mockImplementationOnce((err) => err.message);
const expectedError = new Error('Failed to remove');
mockedContext.dispatch.mockImplementationOnce(() => {
throw new Error('Error');
});
await expect(deleteModule.removeQ(mockedContext, { index: 1, item: mockItem }).then(resFunction, rejFunction))
.resolves.toBe(expectedError.message);
});
test('removeQA succcess', async () => {
const qa = { qid: 'the-one-you-want' };
await deleteModule.removeQA(mockedContext, qa);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'remove', qa.qid);
});
test('removeQA throws error', async () => {
const qa = { qid: 'the-one-you-want' };
util.api.mockImplementationOnce(() => {
throw new Error('test error');
});
await deleteModule.removeQA(mockedContext, qa);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'remove', qa.qid);
expect(mockedContext.commit).toHaveBeenCalledTimes(0);
});
test('removeQAs success', async () => {
const qas = [
{ qid: 'remove-in-bulk' },
{ qid: 'also-remove-in-bulk' },
];
await deleteModule.removeQAs(mockedContext, qas);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'removeBulk', [
'remove-in-bulk',
'also-remove-in-bulk',
]);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('page/decrementTotal', qas.length, { root: true });
});
test('removeQAs failure', async () => {
const qas = [
{ qid: 'remove-in-bulk' },
{ qid: 'also-remove-in-bulk' },
];
util.api.mockImplementationOnce(() => {
throw new Error('test error');
});
await deleteModule.removeQAs(mockedContext, qas);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'removeBulk', [
'remove-in-bulk',
'also-remove-in-bulk',
]);
expect(mockedContext.commit).toHaveBeenCalledTimes(0);
});
test('removeFilter with filter', async () => {
await deleteModule.removeFilter(mockedContext);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'removeQuery', `${mockedContext.state.filter}.*`);
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.commit).toHaveBeenCalledWith('clearQA');
expect(mockedContext.commit).toHaveBeenCalledWith('clearFilter');
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('get', {});
});
test('removeFilter without filter', async () => {
const originalFilter = mockedContext.state.filter;
mockedContext.state.filter = '';
await deleteModule.removeFilter(mockedContext);
mockedContext.state.filter = originalFilter;
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'removeQuery', '.*');
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.commit).toHaveBeenCalledWith('clearQA');
expect(mockedContext.commit).toHaveBeenCalledWith('clearFilter');
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('get', {});
});
test('removeFilter throws an error', async () => {
const resFunction = jest.fn();
const rejFunction = jest.fn().mockImplementationOnce((err) => err.message);
const expectedError = new Error('Failed to remove');
util.api.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(deleteModule.removeFilter(mockedContext).then(resFunction, rejFunction))
.resolves.toBe(expectedError.message);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'removeQuery', `${mockedContext.state.filter}.*`);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/actions/get.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const getModule = require('../../../../../js/lib/store/data/actions/get');
const util = require('../../../../../js/lib/store/data/actions/util');
jest.mock('../../../../../js/lib/store/data/actions/util');
describe('get data action', () => {
const mockedContext = {
commit: jest.fn(),
dispatch: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('schema', async () => {
util.api.mockResolvedValueOnce({});
await getModule.schema(mockedContext);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'schema');
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('schema', {});
});
test('botinfo success', async () => {
const mockValue = { value: '' };
util.api.mockResolvedValue(mockValue);
await getModule.botinfo(mockedContext);
expect(util.api).toHaveBeenCalledTimes(2);
expect(util.api).toHaveBeenCalledWith(mockedContext, 'botinfo');
expect(util.api).toHaveBeenCalledWith(mockedContext, 'alexa');
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.commit).toHaveBeenCalledWith('bot', mockValue, { root: true });
expect(mockedContext.commit).toHaveBeenCalledWith('alexa', mockValue, { root: true });
});
test('botinfo failure', async () => {
const resFunction = jest.fn();
const rejFunction = jest.fn().mockImplementationOnce((err) => err.message);
const expectedError = 'Failed get BotInfo';
util.api.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(getModule.botinfo(mockedContext).then(resFunction, rejFunction))
.resolves.toBe(expectedError);
expect(util.api).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledTimes(0);
});
test('getAll success', async () => {
mockedContext.dispatch
.mockResolvedValueOnce(1)
.mockResolvedValueOnce(0);
await getModule.getAll(mockedContext);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('clearQA');
expect(mockedContext.dispatch).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledWith('get', { page: 0 });
expect(mockedContext.dispatch).toHaveBeenCalledWith('get', { page: 1 });
});
test('getAll failure', async () => {
const mockedError = new Error('test error');
mockedContext.commit.mockImplementationOnce(() => {
throw mockedError;
});
await expect(getModule.getAll(mockedContext)).rejects.toBe(mockedError);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/actions/up-download.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const upDownloadModule = require('../../../../../js/lib/store/data/actions/up-download');
const axios = require('axios');
const util = require('../../../../../js/lib/store/data/actions/util');
const { Validator } = require('jsonschema');
jest.mock('axios');
jest.mock('../../../../../js/lib/store/data/actions/util');
describe('up-download data action', () => {
const mockedContext = {
dispatch: jest.fn(),
commit: jest.fn(),
state: {
QAs: [
{
questions: [
{ text: 'question 1' },
{ text: 'question 2' },
],
answer: {
text: 'test answer',
},
card: {
text: '{ "key": "value" }',
},
qid: {
text: '1',
},
},
],
selectIds: [
'1',
'2',
],
},
};
const mockedResult = {
qa: 'test qna',
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('download success', async () => {
const expectedBlob = new Blob(
[JSON.stringify({ qna: mockedResult.qa }, null, 3)],
{ type: 'text/plain;charset=utf-8' },
);
util.api.mockResolvedValueOnce(mockedResult);
await expect(upDownloadModule.download(mockedContext)).resolves.toEqual(expectedBlob);
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(
mockedContext,
'list',
{ from: 'all' },
);
});
test('download failure', async () => {
const testError = new Error('Failed to download');
util.api.mockImplementationOnce(() => {
throw testError;
});
await expect(upDownloadModule.download(mockedContext)).rejects.toEqual(testError);
expect(util.api).toHaveBeenCalledTimes(1);
});
test('downloadSelect failure', async () => {
const expectedError = new Error('Failed to download the select');
util.api.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(upDownloadModule.downloadSelect(mockedContext)).rejects.toEqual(expectedError);
});
test('upload with data params', async () => {
const expectedOut = {};
const mockedParams = {
data: 'test-data',
};
mockedContext.dispatch.mockResolvedValueOnce(expectedOut);
await upDownloadModule.upload(mockedContext, mockedParams);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('uploadProcess', { data: mockedParams.data });
});
test('upload with url params', async () => {
const expectedOut = {};
const mockedParams = {
url: 'https://example.com',
};
mockedContext.dispatch.mockResolvedValueOnce(expectedOut);
await upDownloadModule.upload(mockedContext, mockedParams);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('uploadUrl', { url: mockedParams.url });
});
test('upload with invalid params', async () => {
const mockedParams = {
unknownKey: 'someValue',
};
const expectedErrorMessage = 'invalid params';
jest.spyOn(Promise, 'reject');
await expect(upDownloadModule.upload(mockedContext, mockedParams))
.rejects.toEqual(expectedErrorMessage);
expect(Promise.reject).toHaveBeenCalledWith(expectedErrorMessage);
});
test('upload throws an error', async () => {
const mockedParams = {
data: 'test-data',
};
const expectedError = new Error('Failed to upload');
mockedContext.dispatch.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(upDownloadModule.upload(mockedContext, mockedParams))
.rejects.toEqual(expectedError);
});
test('uploadProcess has valid qna', async () => {
jest.spyOn(Validator.prototype, 'validate').mockImplementationOnce(() => ({ valid: true }));
await upDownloadModule.uploadProcess(mockedContext, { data: 'test value' });
expect(util.api).toHaveBeenCalledTimes(1);
expect(util.api).toHaveBeenCalledWith(
mockedContext,
'bulk',
'test value',
);
});
test('uploadProcess has invalid qna', async () => {
const validationResult = {
valid: false,
errors: [
{ stack: 'testFunction' },
{ stack: 'testFunction2' },
],
};
const expectedRejectParam = `Invalid QnA:${validationResult.errors.map((err) => err.stack).join(',')}`;
jest.spyOn(Validator.prototype, 'validate').mockImplementationOnce(() => validationResult);
jest.spyOn(Promise, 'reject');
await upDownloadModule.uploadProcess(mockedContext, {});
expect(util.api).toHaveBeenCalledTimes(0);
expect(Promise.reject).toHaveBeenCalledWith(expectedRejectParam);
});
test('uploadProcess throws an error', async () => {
jest.spyOn(Validator.prototype, 'validate').mockImplementationOnce(() => {
throw new Error('test error');
});
const expectedError = new Error('Failed in upload process');
await expect(upDownloadModule.uploadProcess(mockedContext, {})).rejects.toEqual(expectedError);
});
test('uploadUrl success', async () => {
const mockUrl = 'https://test.com'
const mockedResult = {
data: 'test data',
};
axios.get.mockResolvedValueOnce(mockedResult);
await upDownloadModule.uploadUrl(mockedContext, { mockUrl });
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('upload', { data: 'test data' });
});
test('uploadUrl failure', async () => {
const expectedError = new Error('Error: please check URL and source CORS configuration');
axios.get.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(upDownloadModule.uploadUrl(mockedContext, {})).rejects.toEqual(expectedError);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/actions/util.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('../../../../../js/lib/store/data/actions/util');
describe('util data action', () => {
const mockedContext = {
dispatch: jest.fn(),
commit: jest.fn(),
handle: util.handle,
load: util.load,
state: {
QAs: [
{
questions: [
{ text: 'question 1' },
{ text: 'question 2' },
],
answer: {
text: 'test answer',
},
card: {
text: '{ "key": "value" }',
},
qid: {
text: '1',
},
},
],
},
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('dispatch', () => {
const mockedName = 'list';
util.api(mockedContext, mockedName, {});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith(`api/${mockedName}`, {}, { root: true });
});
test('parse', () => {
const item = {
_score: 1,
select: true,
};
const expectedItem = {
_score: 1,
q: [],
t: '',
r: {
title: '',
text: '',
url: '',
},
select: true,
};
const result = util.parse(item);
expect(result).toEqual(expectedItem);
});
test('handle', () => {
const testReason = 'test error';
const handleFunction = mockedContext.handle(testReason);
expect(handleFunction('some error')).rejects.toEqual(testReason);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('setError', testReason, { root: true });
});
test('load without qa', async () => {
const expectedError = new Error('Failed to load');
const inputList = [
'item 1',
'item 2',
];
jest.spyOn(Promise, 'resolve').mockResolvedValueOnce({});
await expect(mockedContext.load(inputList)).rejects.toEqual(expectedError);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/getters.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const gettersModule = require('../../../../js/lib/store/data/getters');
describe('getters data', () => {
const testState = {
QAs: [
{ qid: { text: '2' }, select: true, score: 0.8 },
{ qid: { text: '3' }, select: true, score: 0.9 },
{ qid: { text: '1' }, select: false, score: 0.5 },
],
};
const sortedQAsByQid = [
{ qid: { text: '1' }, select: false, score: 0.5 },
{ qid: { text: '2' }, select: true, score: 0.8 },
{ qid: { text: '3' }, select: true, score: 0.9 },
];
const sortedQAsByScore = [
{ qid: { text: '3' }, select: true, score: 0.9 },
{ qid: { text: '2' }, select: true, score: 0.8 },
{ qid: { text: '1' }, select: false, score: 0.5 },
];
test('selected', () => {
const expectedResult = [true, true, false];
expect(gettersModule.selected(testState)).toEqual(expectedResult);
});
test('QAList with page mode == "test"', () => {
const rootGetters = {
page: {
mode: 'prod',
},
};
expect(gettersModule.QAlist(testState, {}, rootGetters)).toEqual(sortedQAsByQid);
});
test('QAList with page mode !== "test"', () => {
const rootGetters = {
page: {
mode: 'test',
},
};
expect(gettersModule.QAlist(testState, {}, rootGetters)).toEqual(sortedQAsByScore);
});
});
================================================
FILE: source/website/__tests__/lib/store/data/mutations.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mutationsModule = require('../../../../js/lib/store/data/mutations');
describe('mutations data', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('close success', () => {
const testStore = {
commit: jest.fn(),
QAs: [
{
open: true,
edit: true,
questions: [
{ text: 'question 1', tmp: 'question 1' },
{ text: 'question 2', tmp: 'question 2' },
],
answer: {
text: 'test answer',
tmp: 'test answer',
},
card: {
imageUrl: {
text: 'https://example.com', tmp: 'https://example.com',
},
title: {
text: 'test-title', tmp: 'test-title',
},
},
qid: {
text: '1',
tmp: '1',
},
},
],
};
expect(mutationsModule.close(testStore)).toBe(true);
expect(testStore.QAs[0].open).toBe(false);
expect(testStore.QAs[0].edit).toBe(false);
expect(testStore.commit).toHaveBeenCalledTimes(0);
});
test('close fail', () => {
const testStore = {
commit: jest.fn(),
QAs: [
{
open: true,
edit: true,
questions: [
{ text: 'question 1', tmp: 'some other value' },
{ text: 'question 2', tmp: 'question 2' },
],
answer: {
text: 'test answer',
tmp: 'test answer',
},
card: {
imageUrl: {
text: 'https://example.com', tmp: 'https://other-value.example.com',
},
title: {
text: 'test-title', tmp: 'test-title',
},
},
qid: {
text: '1',
tmp: '1',
},
},
],
};
expect(mutationsModule.close(testStore)).toBe(false);
expect(testStore.QAs[0].open).toBe(true);
expect(testStore.QAs[0].edit).toBe(true);
expect(testStore.commit).toHaveBeenCalledTimes(1);
expect(testStore.commit).toHaveBeenCalledWith('setError', 'Please save or cancel your work', { root: true });
});
test('selectAll', () => {
const testStore = {
QAs: [
{ select: false },
{ select: true },
{ select: false },
],
};
mutationsModule.selectAll(testStore, true);
expect(testStore.QAs.map((qa) => qa.select).includes(false)).toBe(false);
});
test('setFilter', () => {
const testStore = {
filter: '',
};
const filterText = 'test-filter';
mutationsModule.setFilter(testStore, filterText);
expect(testStore.filter).toEqual(filterText);
});
test('clearFilter', () => {
const testStore = {
filter: 'test-filter',
};
mutationsModule.clearFilter(testStore);
expect(testStore.filter).toEqual(null);
});
test('schema', () => {
const testState = {
schema: '',
};
const testSchema = 'test-schema';
mutationsModule.schema(testState, testSchema);
expect(testState.schema).toEqual(testSchema);
});
test('delQA', () => {
const testState = {
QAs: [
{ qid: '1' },
{ qid: '2' },
{ qid: '3' },
],
};
const QaToDelete = {
qid: '2',
};
mutationsModule.delQA(testState, QaToDelete);
expect(testState.QAs.length).toBe(2);
expect(testState.QAs.findIndex((qa) => qa.qid === QaToDelete.qid)).toBe(-1);
});
test('clearQA', () => {
const testState = {
QAs: [
{ qid: '1' },
{ qid: '2' },
{ qid: '3' },
],
};
mutationsModule.clearQA(testState);
expect(testState.QAs.length).toBe(0);
});
test('results', () => {
const testState = {
results: 'test-result',
};
const newResult = 'new-result';
mutationsModule.results(testState, newResult);
expect(testState.results).toEqual(newResult);
});
});
================================================
FILE: source/website/__tests__/lib/store/page/actions.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const actionsModule = require('../../../../js/lib/store/page/actions');
describe('actions page test', () => {
const mockedContext = {
commit: jest.fn(),
dispatch: jest.fn(),
state: {},
};
beforeEach(() => {
const stateDefault = {
current: 5,
total: 10,
perpage: 1,
};
mockedContext.state = { ...stateDefault };
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('setMode mode == questions', () => {
const mode = 'questions';
actionsModule.setMode(mockedContext, mode);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('setMode', mode);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('goToPage', mockedContext.state.current);
});
test('setMode', () => {
const mode = 'test';
actionsModule.setMode(mockedContext, mode);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(0);
});
test('goToPage', async () => {
const index = 5;
await expect(actionsModule.goToPage(mockedContext, index)).resolves.not.toThrow();
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('data/get', index, { root: true });
});
test('goToPage throws an error', async () => {
const index = 5;
const testError = new Error(('Failed to Build'));
mockedContext.dispatch.mockImplementationOnce(() => {
throw new Error('test error');
});
await expect(actionsModule.goToPage(mockedContext, index)).rejects.toEqual(testError);
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('data/get', index, { root: true });
});
test('nextPage', () => {
mockedContext.state.current = mockedContext.state.total - 3;
actionsModule.nextPage(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('goToPage', mockedContext.state.total - 2);
});
test('nextPage at last page', () => {
mockedContext.state.current = mockedContext.state.total - 1;
actionsModule.nextPage(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('goToPage', mockedContext.state.total - 1);
});
test('previousPage', () => {
mockedContext.state.current = 4;
actionsModule.previousPage(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('goToPage', mockedContext.state.current - 1);
});
test('previousPage at first page', () => {
mockedContext.state.current = 1;
actionsModule.previousPage(mockedContext);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('goToPage', 0);
});
});
================================================
FILE: source/website/__tests__/lib/store/page/getters.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const gettersModule = require('../../../../js/lib/store/page/getters');
describe('getters page test', () => {
test('pages', () => {
const state = {
total: 10,
perpage: 5,
}
const expectedPages = 2;
expect(gettersModule.pages(state)).toEqual(expectedPages);
});
});
================================================
FILE: source/website/__tests__/lib/store/page/mutations.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mutations = require('../../../../js/lib/store/page/mutations');
describe('mutations page test', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('setMode', () => {
const store = {
mode: '',
};
const newMode = 'test';
mutations.setMode(store, newMode);
expect(store.mode).toEqual(newMode);
});
test('setPage', () => {
const store = {
current: 3,
};
const newPage = 7;
mutations.setPage(store, newPage);
expect(store.current).toEqual(newPage);
});
test('setTotal', () => {
const store = {
total: 6,
};
const newTotal = 7;
mutations.setTotal(store, newTotal);
expect(store.total).toEqual(newTotal);
});
test('incrementTotal by 1', () => {
const store = {
page: 6,
};
const expectedResult = store.page + 1;
mutations.incrementTotal(store);
expect(store.page).toEqual(expectedResult);
});
test('incrementTotal by x', () => {
const store = {
page: 6,
};
const expectedResult = store.page + 3;
mutations.incrementTotal(store, 3);
expect(store.page).toEqual(expectedResult);
});
test('decrementTotal by 1', () => {
const store = {
page: 6,
};
const expectedResult = store.page - 1;
mutations.decrementTotal(store);
expect(store.page).toEqual(expectedResult);
});
test('decrementTotal by x', () => {
const store = {
page: 6,
};
const expectedResult = store.page - 2;
mutations.decrementTotal(store, 2);
expect(store.page).toEqual(expectedResult);
});
test('toggleMode filter mode', () => {
const mode = 'filter';
const store = {
mode: {
filter: {
on: false,
},
other: true,
someBooleanKey: true,
},
};
mutations.toggleMode(store, mode);
expect(store.mode.filter.on).toBe(true);
expect(store.mode.someBooleanKey).toBe(false);
expect(store.mode.other).toBe(false);
});
test('toggleMode other mode', () => {
const mode = 'other';
const store = {
mode: {
filter: {
on: true,
},
other: false,
someBooleanKey: true,
},
};
mutations.toggleMode(store, mode);
expect(store.mode.filter.on).toBe(false);
expect(store.mode.someBooleanKey).toBe(false);
expect(store.mode.other).toBe(true);
});
test('toggleSearch', () => {
const store = {
mode: {
search: false,
},
};
mutations.toggleSearch(store);
expect(store.mode.search).toBe(true);
});
test('toggleFilter', () => {
const store = {
mode: {
filter: false,
},
};
mutations.toggleFilter(store);
expect(store.mode.filter).toBe(true);
});
});
================================================
FILE: source/website/__tests__/lib/store/page/util.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const utilModule = require('../../../../js/lib/store/page/util');
describe('util page', () => {
const mockedContext = {
dispatch: jest.fn(),
commit: jest.fn(),
handle: utilModule.handle,
load: utilModule.load,
state: {
selectIds: ['1', '5'],
QAs: [{}],
},
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
});
test('api', () => {
const name = 'list';
const args = {};
utilModule.api(mockedContext, name, args);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith(`api/${name}`, args, { root: true });
});
test('parse', () => {
const item = {
score: 0.99,
body: {
r: {
title: 'test-title',
imageUrl: 'https://test-title.example.com',
},
qid: '1',
a: 'this is the way',
t: 'test-topic',
q: [
'question 1',
'question 2',
'question 3',
],
},
};
const expectedResult = {
qid: {
text: item.body.qid,
tmp: item.body.qid,
},
answer: {
text: item.body.a,
tmp: item.body.a,
},
card: {
text: JSON.stringify(item.body.r, null, 4),
title: {
text: item.body.r.title,
tmp: item.body.r.title,
},
imageUrl: {
text: item.body.r.imageUrl,
tmp: item.body.r.imageUrl,
},
},
topic: {
text: item.body.t,
tmp: item.body.t,
},
questions: [
{ text: item.body.q[0], tmp: item.body.q[0] },
{ text: item.body.q[1], tmp: item.body.q[1] },
{ text: item.body.q[2], tmp: item.body.q[2] },
],
open: false,
edit: false,
select: true,
deleting: false,
score: item.score,
};
expect(utilModule.parse(item, mockedContext)).toEqual(expectedResult);
});
test('parse using defaults', () => {
const item = {
body: {
qid: '4',
a: 'this is the way',
q: [
'question 1',
'question 2',
'question 3',
],
},
};
const defaultR = {
title: '',
imageUrl: '',
};
const expectedResult = {
qid: {
text: item.body.qid,
tmp: item.body.qid,
},
answer: {
text: item.body.a,
tmp: item.body.a,
},
card: {
text: JSON.stringify(defaultR, null, 4),
title: {
text: '',
tmp: '',
},
imageUrl: {
text: '',
tmp: '',
},
},
topic: {
text: '',
tmp: '',
},
questions: [
{ text: item.body.q[0], tmp: item.body.q[0] },
{ text: item.body.q[1], tmp: item.body.q[1] },
{ text: item.body.q[2], tmp: item.body.q[2] },
],
open: false,
edit: false,
select: false,
deleting: false,
score: 0,
};
expect(utilModule.parse(item, mockedContext)).toEqual(expectedResult);
});
test('handle', async () => {
const reason = 'test reason';
await expect(mockedContext.handle(reason)).rejects.toBe(reason);
expect(mockedContext.commit).toHaveBeenCalledTimes(1);
expect(mockedContext.commit).toHaveBeenCalledWith('setError', reason, { root: true });
});
test('load fails to access qa', async () => {
const expectedError = new Error('Failed to load');
jest.spyOn(Promise, 'resolve').mockResolvedValueOnce({});
await expect(mockedContext.load([])).rejects.toEqual(expectedError);
expect(mockedContext.commit).toHaveBeenCalledTimes(2);
expect(mockedContext.commit).toHaveBeenCalledWith('startLoading');
expect(mockedContext.commit).toHaveBeenCalledWith('stopLoading');
});
});
================================================
FILE: source/website/__tests__/lib/store/user/actions.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/**
* @jest-environment jsdom
*/
const { fromCognitoIdentityPool } = require('@aws-sdk/credential-providers');
const actionsModule = require('../../../../js/lib/store/user/actions');
const axios = require('axios');
const query = require('query-string');
const jwt = require('jsonwebtoken');
const { mockClient } = require('aws-sdk-client-mock');
const { CognitoIdentityProviderClient, AdminUserGlobalSignOutCommand } = require('@aws-sdk/client-cognito-identity-provider');
const cognitoIdentityProviderClientMock = mockClient(CognitoIdentityProviderClient);
require('aws-sdk-client-mock-jest');
jest.mock('@aws-sdk/credential-providers');
jest.mock('@aws-sdk/client-cognito-identity-provider')
jest.mock('axios');
jest.mock('jsonwebtoken');
describe('user actions test', () => {
let windowSpy;
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(console, 'log').mockImplementation(jest.fn());
windowSpy = jest.spyOn(window, 'window', 'get');
windowSpy.mockReturnValue({
alert: jest.fn(),
confirm: jest.fn(),
sessionStorage: {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
},
window: {
location: {
href: '',
},
},
location: {
search: '?code=200',
origin: 'test.origin',
pathname: '/test/path',
replace: jest.fn(),
},
localStorage: {
clear: jest.fn(),
},
});
cognitoIdentityProviderClientMock.reset();
});
afterEach(() => {
windowSpy.mockRestore();
jest.resetAllMocks();
cognitoIdentityProviderClientMock.restore();
});
test('refresh tokens', async () => {
const mockedContext = {
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
state: {
token: '',
},
};
const testIdToken = 'test-id-token';
const testAccessToken = 'test-access-token';
const testRefreshToken = 'test-refresh-token';
const mockedTokens = {
data: {
id_token: testIdToken,
access_token: testAccessToken,
refresh_token: testRefreshToken,
},
};
window.sessionStorage.getItem.mockReturnValueOnce(testRefreshToken);
axios.mockReturnValueOnce(mockedTokens);
await actionsModule.refreshTokens(mockedContext);
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith({
method: 'POST',
url: `${mockedContext.rootState.info._links.CognitoEndpoint.href}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'refresh_token',
client_id: mockedContext.rootState.info.ClientIdDesigner,
refresh_token: testRefreshToken,
}),
});
expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(3);
expect(window.sessionStorage.setItem).toHaveBeenCalledWith('id_token', testIdToken);
expect(window.sessionStorage.setItem).toHaveBeenCalledWith('access_token', testAccessToken);
expect(window.sessionStorage.setItem).toHaveBeenCalledWith('refresh_token', testRefreshToken);
expect(mockedContext.state.token).toEqual(testIdToken);
});
test('refresh tokens -- expired credentials 1', async () => {
const mockedContext = {
dispatch: jest.fn(),
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
state: {
token: '',
},
};
const testRefreshToken = 'test-refresh-token';
const testError = new Error('test-error');
window.sessionStorage.getItem.mockReturnValueOnce(testRefreshToken);
window.confirm.mockReturnValue(true);
axios.mockReturnValueOnce(testError);
await actionsModule.refreshTokens(mockedContext);
expect(axios).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('logout');
expect(window.location.href) .toEqual(mockedContext.rootState.info._links.DesignerLogin.href);
});
test('refresh tokens -- expired credentials 2', async () => {
const mockedContext = {
dispatch: jest.fn(),
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
state: {
token: '',
},
};
const testRefreshToken = 'test-refresh-token';
const testError = new Error('test-error');
window.sessionStorage.getItem.mockReturnValueOnce(testRefreshToken);
window.confirm.mockReturnValue(false);
axios.mockReturnValueOnce(testError);
await actionsModule.refreshTokens(mockedContext);
expect(axios).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledTimes(0);
});
test('get credentials -- no credentials', async () => {
const mockedContext = {
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: '',
},
};
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce({}));
await expect(actionsModule.getCredentials(mockedContext)).resolves.toEqual({});
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(1);
expect(fromCognitoIdentityPool).toHaveBeenCalledWith({
identityPoolId: mockedContext.rootState.info.PoolId,
logins,
clientConfig: { region: mockedContext.rootState.info.region },
});
});
test('get credentials -- renew credentials', async () => {
const mockedContext = {
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
},
};
const mockedNewCredentials = {
expiration: new Date(Date.now() + 1000),
};
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce(mockedNewCredentials));
await expect(actionsModule.getCredentials(mockedContext))
.resolves.toEqual(mockedNewCredentials);
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(1);
expect(fromCognitoIdentityPool).toHaveBeenCalledWith({
identityPoolId: mockedContext.rootState.info.PoolId,
logins,
clientConfig: { region: mockedContext.rootState.info.region },
});
});
test('get credentials -- non-expiring credentials', async () => {
const mockedContext = {
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: 'test-token',
credentials: {
expiration: '',
},
},
};
const expectedCredentials = {
expiration: '',
};
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce(expectedCredentials));
await expect(actionsModule.getCredentials(mockedContext))
.resolves.toEqual(expectedCredentials);
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(0);
});
test('get credentials -- throws expired token error', async () => {
const mockedContext = {
dispatch: jest.fn(),
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
},
};
const mockedNewCredentials = {
expiration: new Date(Date.now() + 1000),
};
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool
.mockReturnValueOnce(jest.fn().mockImplementation(() => {
throw new Error('Token expired');
}))
.mockReturnValueOnce(jest.fn().mockReturnValueOnce(mockedNewCredentials));
await expect(actionsModule.getCredentials(mockedContext))
.resolves.toEqual(mockedNewCredentials);
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(2);
expect(fromCognitoIdentityPool).toHaveBeenCalledWith({
identityPoolId: mockedContext.rootState.info.PoolId,
logins,
clientConfig: { region: mockedContext.rootState.info.region },
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('refreshTokens');
});
test('get credentials -- throws inactive token error', async () => {
const mockedContext = {
dispatch: jest.fn(),
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
},
};
const mockedNewCredentials = {
expiration: new Date(Date.now() + 1000),
};
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool
.mockReturnValueOnce(jest.fn().mockImplementation(() => {
throw new Error('inactive');
}))
.mockReturnValueOnce(jest.fn().mockReturnValueOnce(mockedNewCredentials));
await expect(actionsModule.getCredentials(mockedContext))
.resolves.toEqual(mockedNewCredentials);
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(2);
expect(fromCognitoIdentityPool).toHaveBeenCalledWith({
identityPoolId: mockedContext.rootState.info.PoolId,
logins,
clientConfig: { region: mockedContext.rootState.info.region },
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('refreshTokens');
});
test('get credentials -- throws unknown error', async () => {
const mockedContext = {
dispatch: jest.fn(),
rootState: {
info: {
region: 'us-weast-1',
PoolId: 'XXXXXXXXX',
},
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
},
};
const unexpectedError = new Error('Some other error');
const logins = {};
logins[[
'cognito-idp.',
mockedContext.rootState.info.region,
'.amazonaws.com/',
mockedContext.rootState.info.UserPool,
].join('')] = mockedContext.state.token;
fromCognitoIdentityPool
.mockReturnValueOnce(jest.fn().mockImplementation(() => {
throw unexpectedError;
}))
await expect(actionsModule.getCredentials(mockedContext))
.rejects.toEqual(unexpectedError);
expect(fromCognitoIdentityPool).toHaveBeenCalledTimes(1);
expect(fromCognitoIdentityPool).toHaveBeenCalledWith({
identityPoolId: mockedContext.rootState.info.PoolId,
logins,
clientConfig: { region: mockedContext.rootState.info.region },
});
expect(mockedContext.dispatch).toHaveBeenCalledTimes(0);
});
test('is able to logout and global sign out', async () => {
const expectedCredentials = {
accessKeyId: 'mock-access-key',
secretAccessKey: 'mock-secret-key',
sessionToken: 'mock-session-token',
expiration: new Date(Date.now() - 1000)
};
const mockedContext = {
rootState: {
info: {
region: 'mock-region',
UserPool: 'mock-user-pool-id',
ClientIdDesigner: 'mock-client-id',
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
},
user: {
name: 'some-user',
credentials: {
expiration: new Date(Date.now() - 1000),
},
}
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
}
};
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce(expectedCredentials));
cognitoIdentityProviderClientMock.on(AdminUserGlobalSignOutCommand).resolvesOnce({
$metadata: {
httpStatusCode: 200, // successful response
}
})
const expectedLogoutUrl = `${mockedContext.rootState.info._links.CognitoEndpoint.href}/logout?response_type=code&client_id=${mockedContext.rootState.info.ClientIdDesigner}&redirect_uri=test.origin/test/path`
await actionsModule.logout(mockedContext);
expect(cognitoIdentityProviderClientMock).toHaveReceivedCommandTimes(AdminUserGlobalSignOutCommand, 1);
expect(window.sessionStorage.clear).toHaveBeenCalledTimes(1);
expect(window.localStorage.clear).toHaveBeenCalledTimes(1);
expect(mockedContext.rootState.user.name).toEqual('some-user')
expect(mockedContext.state.credentials).toEqual(undefined)
expect(mockedContext.rootState.user.credentials).toEqual(undefined)
expect(window.location.replace).toHaveBeenCalledWith(
expect.stringContaining(expectedLogoutUrl)
);
});
test('can logout when error occurs in credentials provider', async () => {
const mockedContext = {
rootState: {
info: {
region: 'mock-region',
UserPool: 'mock-user-pool-id',
ClientIdDesigner: 'mock-client-id',
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
},
user: {
name: 'some-user',
credentials: {
expiration: new Date(Date.now() - 1000),
},
}
},
};
fromCognitoIdentityPool
.mockReturnValueOnce(jest.fn().mockImplementation(() => {
throw new Error('unexpected credentials error');
}))
const expectedLogoutUrl = `${mockedContext.rootState.info._links.CognitoEndpoint.href}/logout?response_type=code&client_id=${mockedContext.rootState.info.ClientIdDesigner}&redirect_uri=test.origin/test/path`
await actionsModule.logout(mockedContext);
expect(cognitoIdentityProviderClientMock).toHaveReceivedCommandTimes(AdminUserGlobalSignOutCommand, 0);
expect(window.sessionStorage.clear).toHaveBeenCalledTimes(1);
expect(window.localStorage.clear).toHaveBeenCalledTimes(1);
expect(mockedContext.rootState.user.name).toEqual('some-user')
expect(mockedContext.state).toEqual(undefined)
expect(mockedContext.rootState.user.credentials).toEqual(undefined)
expect(window.location.replace).toHaveBeenCalledWith(
expect.stringContaining(expectedLogoutUrl)
);
});
test('can logout when error occurs during global signout', async () => {
const expectedCredentials = {
accessKeyId: 'mock-access-key',
secretAccessKey: 'mock-secret-key',
sessionToken: 'mock-session-token',
expiration: new Date(Date.now() - 1000)
};
const mockedContext = {
rootState: {
info: {
region: 'mock-region',
UserPool: 'mock-user-pool-id',
ClientIdDesigner: 'mock-client-id',
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
},
user: {
name: 'some-user',
credentials: {
expiration: new Date(Date.now() - 1000),
},
}
},
state: {
token: 'test-token',
credentials: {
expiration: new Date(Date.now() - 1000),
},
}
};
fromCognitoIdentityPool.mockReturnValueOnce(jest.fn().mockReturnValueOnce(expectedCredentials));
cognitoIdentityProviderClientMock.on(AdminUserGlobalSignOutCommand).rejectsOnce(new Error('unexpected global signout error'));
const expectedLogoutUrl = `${mockedContext.rootState.info._links.CognitoEndpoint.href}/logout?response_type=code&client_id=${mockedContext.rootState.info.ClientIdDesigner}&redirect_uri=test.origin/test/path`
await actionsModule.logout(mockedContext);
expect(cognitoIdentityProviderClientMock).toHaveReceivedCommandTimes(AdminUserGlobalSignOutCommand, 1);
expect(window.sessionStorage.clear).toHaveBeenCalledTimes(1);
expect(window.localStorage.clear).toHaveBeenCalledTimes(1);
expect(mockedContext.rootState.user.name).toEqual('some-user')
expect(mockedContext.state.credentials).toEqual(undefined)
expect(mockedContext.rootState.user.credentials).toEqual(undefined)
expect(window.location.replace).toHaveBeenCalledWith(
expect.stringContaining(expectedLogoutUrl)
);
});
test('login -- id_token exists', async () => {
const mockedContext = {
state: {
token: '',
name: '',
groups: '',
},
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
};
const testIdToken = 'test-id-token';
const testAccessToken = 'test-access-token';
const testRefreshToken = 'test-refresh-token';
const mockedTokens = {
data: {
id_token: testIdToken,
access_token: testAccessToken,
refresh_token: testRefreshToken,
},
};
const testToken = {
'cognito:username': 'testusername',
'cognito:groups': 'Admins',
};
window.sessionStorage.getItem.mockReturnValueOnce(testIdToken);
axios.mockReturnValueOnce(mockedTokens);
jwt.decode.mockReturnValue(testToken);
await actionsModule.login(mockedContext);
expect(jwt.decode).toHaveBeenCalledTimes(1);
expect(jwt.decode).toHaveBeenCalledWith(testIdToken);
expect(mockedContext.state.name).toEqual(testToken['cognito:username']);
expect(mockedContext.state.groups).toEqual(testToken['cognito:groups']);
// The assertion below becomes false when the getTokens function is called.
expect(axios).toHaveBeenCalledTimes(0);
// The alert window should not be called since the user belongs to Admins group.
expect(window.alert).toHaveBeenCalledTimes(0);
});
test('login -- id_token does not exist', async () => {
const mockedContext = {
state: {
token: '',
name: '',
groups: '',
},
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
};
const testIdToken = 'test-id-token';
const testAccessToken = 'test-access-token';
const testRefreshToken = 'test-refresh-token';
const mockedTokens = {
data: {
id_token: testIdToken,
access_token: testAccessToken,
refresh_token: testRefreshToken,
},
};
const testToken = {
'cognito:username': 'testusername',
'cognito:groups': 'testgroup',
};
axios.mockReturnValueOnce(mockedTokens);
jwt.decode.mockReturnValue(testToken);
await actionsModule.login(mockedContext);
expect(jwt.decode).toHaveBeenCalledTimes(1);
expect(jwt.decode).toHaveBeenCalledWith(testIdToken);
expect(mockedContext.state.name).toEqual(testToken['cognito:username']);
expect(mockedContext.state.groups).toEqual(testToken['cognito:groups']);
expect(window.alert).toHaveBeenCalledTimes(1);
expect(window.location.href).toEqual(mockedContext.rootState.info._links.DesignerLogin.href);
// The assertions below become true when the getTokens function is called.
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith({
method: 'POST',
url: `${mockedContext.rootState.info._links.CognitoEndpoint.href}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'authorization_code',
client_id: mockedContext.rootState.info.ClientIdDesigner,
code: 200,
redirect_uri: window.location.origin + window.location.pathname,
}),
});
expect(mockedContext.state.token).toEqual(testIdToken);
});
test('login -- unable to fetch credentials 1', async () => {
const mockedContext = {
dispatch: jest.fn(),
state: {
token: '',
name: '',
groups: '',
},
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
};
const testToken = {
'cognito:username': 'testusername',
'cognito:groups': 'testgroup',
};
const testError = new Error('test error');
axios.mockReturnValueOnce(testError);
jwt.decode.mockReturnValue(testToken);
window.confirm.mockReturnValueOnce(true);
await actionsModule.login(mockedContext);
// The assertions below become true when the getTokens function is called.
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith({
method: 'POST',
url: `${mockedContext.rootState.info._links.CognitoEndpoint.href}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'authorization_code',
client_id: mockedContext.rootState.info.ClientIdDesigner,
code: 200,
redirect_uri: window.location.origin + window.location.pathname,
}),
});
expect(window.confirm).toHaveBeenCalledTimes(1);
expect(window.confirm).toHaveBeenCalledWith('Unable to fetch credentials, please log back in. Click Ok to be redirected to the login page.');
expect(mockedContext.dispatch).toHaveBeenCalledTimes(1);
expect(mockedContext.dispatch).toHaveBeenCalledWith('logout');
});
test('login -- unable to fetch credentials 2', async () => {
const mockedContext = {
dispatch: jest.fn(),
state: {
token: '',
name: '',
groups: '',
},
rootState: {
info: {
_links: {
CognitoEndpoint: {
href: 'some.cognito.endpoint',
},
DesignerLogin: {
href: 'some.login.endpoint',
},
},
ClientIdDesigner: 'XXXXXXXXX',
},
},
};
const testToken = {
'cognito:username': 'testusername',
'cognito:groups': 'testgroup',
};
const testError = new Error('test error');
axios.mockReturnValueOnce(testError);
jwt.decode.mockReturnValue(testToken);
window.confirm.mockReturnValueOnce(false);
await actionsModule.login(mockedContext);
// The assertions below become true when the getTokens function is called.
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith({
method: 'POST',
url: `${mockedContext.rootState.info._links.CognitoEndpoint.href}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'authorization_code',
client_id: mockedContext.rootState.info.ClientIdDesigner,
code: 200,
redirect_uri: window.location.origin + window.location.pathname,
}),
});
expect(window.confirm).toHaveBeenCalledTimes(1);
expect(window.confirm).toHaveBeenCalledWith('Unable to fetch credentials, please log back in. Click Ok to be redirected to the login page.');
expect(mockedContext.dispatch).toHaveBeenCalledTimes(0);
});
});
================================================
FILE: source/website/__tests__/lib/store/user/getters.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const gettersModule = require('../../../../js/lib/store/user/getters');
describe('user getters', () => {
test('it exists', () => {
expect(gettersModule).toBeDefined();
});
});
================================================
FILE: source/website/__tests__/lib/store/user/index.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const indexModule = require('../../../../js/lib/store/user/index');
describe('user index module', () => {
test('it exists', () => {
expect(indexModule).toBeDefined();
expect(indexModule.actions).toBeDefined();
expect(indexModule.mutations).toBeDefined();
expect(indexModule.state).toBeDefined();
expect(indexModule.getters).toBeDefined();
expect(indexModule.namespaced).toBeTruthy();
expect(indexModule.state.loggedin).not.toBeTruthy();
});
});
================================================
FILE: source/website/__tests__/lib/store/user/mutations.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const mutationsModule = require('../../../../js/lib/store/user/mutations');
const { set } = require('vue');
jest.mock('vue');
describe('user mutations', () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('credentials', () => {
const mockedState = {
loggedin: false,
creddentials: {},
};
const payload = {
key: 'value',
};
mutationsModule.credentials(mockedState, payload);
expect(mockedState.credentials).toEqual(payload);
expect(mockedState.loggedin).toBe(true);
});
test('login', () => {
const mockedState = {
loggedIn: false,
};
mutationsModule.login(mockedState);
expect(mockedState.loggedIn).toBe(true);
});
test('setId', () => {
const mockedState = {
Id: '',
};
const newId = 'newId';
mutationsModule.setId(mockedState, newId);
expect(mockedState.Id).toBe(newId);
});
});
================================================
FILE: source/website/__tests__/resolver.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
// Call the defaultResolver, so we leverage its cache, error handling, etc.
module.exports = (path, options) => options.defaultResolver(path, {
...options,
packageFilter: (pkg) => {
// This is a workaround for https://github.com/uuidjs/uuid/pull/616
//
// jest-environment-jsdom 28+ tries to use browser exports instead of default exports,
// but uuid only offers an ESM browser export and not a CommonJS one. Jest does not yet
// support ESM modules natively, so this causes a Jest error related to trying to parse
// "export" syntax.
//
// This workaround prevents Jest from considering uuid's module-based exports at all;
// it falls back to uuid's CommonJS+node "main" property.
//
// Once we're able to migrate our Jest config to ESM and a browser crypto
// implementation is available for the browser+ESM version of uuid to use (eg, via
// https://github.com/jsdom/jsdom/pull/3352 or a similar polyfill), this can go away.
if (pkg.name === 'uuid') {
delete pkg['exports'];
delete pkg['module'];
}
return pkg;
},
});
================================================
FILE: source/website/__tests__/styleMock.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {};
================================================
FILE: source/website/__tests__/test.test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import gremlins from '../assets/gremlins.min';
const testModule = require('../js/test');
describe('test test', () => {
test('exec', () => {
jest.spyOn(gremlins, 'createHorde').mockReturnValue({
gremlin: jest.fn(),
unleash: jest.fn(),
});
jest.spyOn(window.horde, 'unleash').mockImplementation(() => {});
const documentSpy = jest.spyOn(document, 'getElementById')
.mockReturnValueOnce({ hidden: false })
.mockReturnValueOnce(undefined);
window.start(false);
window.start(true);
expect(documentSpy).toHaveBeenCalledTimes(2);
});
});
================================================
FILE: source/website/assets/zombie.json
================================================
{
"qna": [
{
"qid": "zombie.1",
"q": [
"What are Zombies"
],
"a": "Zombies are the undead"
},
{
"qid": "zombie.2",
"q": [
"What do zombies eat?"
],
"a": "Zombies eat brains"
},
{
"qid": "zombie.3",
"q": [
"Do zombies make good pets"
],
"a": "better than cats"
}
]
}
================================================
FILE: source/website/config/base.config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const path = require('path');
const {VueLoaderPlugin} = require('vue-loader');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
module.exports={
entry:{
main:['./entry.js'],
check:['./js/browser-check.js'],
client:['./js/client.js'],
test:['./js/test.js'],
},
output:{
path:path.join(__dirname,'../build'),
filename:'[name].js',
chunkFilename: '[name].js',
},
plugins:[
new VueLoaderPlugin(),
new NodePolyfillPlugin(),
new CopyWebpackPlugin({ patterns: [{from:'./assets',to:'assets'}] }),
new CopyWebpackPlugin({ patterns: [{from:'./styles',to:'styles'}] }),
new CopyWebpackPlugin({ patterns: [{
from:'../node_modules/aws-lex-web-ui/dist/wav-worker.min.js',
to:'wav-worker.js'
}]}),
new HtmlWebpackPlugin({
template:'./html/admin.pug',
filename:'index.html',
chunks:['main','check', 'vendor'],
}),
new HtmlWebpackPlugin({
template:'./html/test.ejs',
filename:'test.html',
chunks:['main','test','check'],
}),
new HtmlWebpackPlugin({
template:'./html/client.pug',
filename:'client.html',
chunks:['client', 'vendor'],
}),
new HtmlWebpackPlugin({
filename:'health.html',
templateContent:'ok\n',
inject:false
}),
new TerserPlugin({
terserOptions: {
output: {
ascii_only: true
}
}
}),
new webpack.DefinePlugin({
__VUE_PROD_DEVTOOLS__: JSON.stringify(true),
__VUE_OPTIONS_API__: JSON.stringify(true),
})
],
resolve: {
modules: [path.resolve(__dirname, '../../'), 'node_modules'],
extensions: [ '.js', '.vue', '.pug', '.styl', '.scss', '.css' ],
alias: {
handlebars: 'handlebars/dist/handlebars.min.js',
Vue: 'vue',
Vuex: 'vuex',
Vuetify: 'vuetify',
},
},
module: {
rules: [
{
test: /\.js$/,
exclude: [
/node_modules/,
/__tests__/,
],
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
{
test: /\.(md|txt)$/,
type: 'asset/source'
},
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.(png|eot|ttf|svg)$/,
type: 'asset/resource'
},
{
test: /\.(woff|woff2)$/,
type: 'asset/resource',
generator : {
filename : './fonts/[name][ext]'
}
},
{
test: /\.pug$/,
oneOf: [
{
resourceQuery: /^\?vue/,
use: ['pug-plain-loader'],
},
{
use: ['pug-loader']
}
]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.styl$/,
use: ['style-loader','css-loader','stylus-loader']
},
{
test: /\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
]
},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
optimization: {
splitChunks: {
cacheGroups: {
'vendor-aws-sdk': {
name: 'vendor-aws-sdk',
test: /[\\/]node_modules[\\/]aws-sdk/,
chunks: 'initial',
idHint: 'vendor-aws-sdk',
},
'vendor-aws-lex-web-ui': {
name: 'vendor-aws-lex-web-ui',
test: /[\\/]node_modules[\\/]aws-lex-web-ui/,
chunks: 'initial',
idHint: 'aws-lex-web-ui',
},
'vendor-vue-vuetify': {
name: 'vendor-vue-vuetify',
test: /[\\/]node_modules[\\/](vuetify|vue)/,
chunks: 'initial',
idHint: 'vendor-vue-vuetify',
}
}
}
},
}
================================================
FILE: source/website/config/dev.config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const config = require('../../config.json');
const path = require('path');
const S3Plugin = require('webpack-s3-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const _ = require('lodash');
const { fromEnv } = require('@aws-sdk/credential-providers');
const { credentials } = fromEnv();
let assetBucketName;
if (process.env.ASSET_BUCKET_NAME === '') {
throw new Error('ASSET_BUCKET_NAME must be set. See README.md for instruction');
} else {
assetBucketName = process.env.ASSET_BUCKET_NAME;
}
module.exports = {
mode: 'development',
watch: true,
watchOptions: {
aggregateTimeout: 500,
},
plugins:_.compact([
new BundleAnalyzerPlugin(),
new S3Plugin({
s3Options: {
credentials,
region: config.region,
},
s3UploadOptions: {
Bucket: assetBucketName,
ACL: '',
},
}),
new ProgressBarPlugin(),
]),
};
================================================
FILE: source/website/config/prod.config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const ZipPlugin = require('zip-webpack-plugin');
module.exports = {
mode: 'production',
plugins:[
new ZipPlugin({
path: '../../build',
filename: 'website.zip',
})
]
}
================================================
FILE: source/website/config/test.config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const config = require('../../config.json');
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const FaviconPlugin = require('favicons-webpack-plugin');
const webpack = require('webpack');
const _ = require('lodash');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractSass = new ExtractTextPlugin({
filename: "[name].css"
});
module.exports={
target:"node",
entry:{
"unit-test":"./test/unit.js"
},
output:{
path:path.join(__dirname,'../test'),
filename:"compiled.js",
libraryTarget:"commonjs2"
},
plugins:_.compact([
]),
resolve:{
alias:{
vue$:'vue/dist/vue.js',
handlebars: 'handlebars/dist/handlebars.min.js',
querystring: 'querystring-browser'
}
},
module: {
rules: [
{
test: /\.(md|txt)$/,
loader: 'raw-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
}
}
},
{
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
},
{
test: /\.pug$/,
loader: 'pug-loader'
},
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.styl$/,
use: ['style-loader','css-loader','stylus-loader']
},
{
test: /\.scss$/,
use: extractSass.extract({
use:[
{loader: "css-loader" },
{loader: "sass-loader" }
]
})
}
]
}
}
================================================
FILE: source/website/config/webpack.config.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const config = require('../../config.json');
process.env.AWS_PROFILE = config.profile;
process.env.AWS_DEFAULT_REGION = config.region;
const { merge } = require('webpack-merge');
const path = require('path');
const webpack = require('webpack');
const base = require('./base.config');
const dev = require('./dev.config');
const prod = require('./prod.config');
module.exports = process.env.NODE_ENV === 'dev' ? merge(base, dev) : merge(base, prod);
================================================
FILE: source/website/entry.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/varela-round';
import '@fontsource/material-icons';
const js = require('./js/admin.js');
================================================
FILE: source/website/html/admin.pug
================================================
html
head
title QnABot Designer
meta(name="viewport" content="width=device-width, initial-scale=1")
link(href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=" rel="icon" type="image/x-icon")
link(href="styles/fonts/material-icons.css" rel="stylesheet" type="text/css")
link(href="styles/pure-min.css" rel="stylesheet" type="text/css")
body
#App
================================================
FILE: source/website/html/client.pug
================================================
html
head
title QnABot Client
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=" rel="icon" type="image/x-icon")
link(href="styles/fonts/material-icons.css" rel="stylesheet" type="text/css")
style #lex-web { height:100%; }
body
#App
================================================
FILE: source/website/html/test.ejs
================================================
FAQ Admin
================================================
FILE: source/website/js/admin.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import app from './admin.vue';
const Idle = require('idle-js');
const { createApp } = require('vue');
require('highlight.js/styles/solarized-light.css');
const { createRouter } = require('vue-router');
const { sync } = require('vuex-router-sync');
const { createVuetify } = require('vuetify');
const components = require('vuetify/components');
const directives = require('vuetify/directives');
const { aliases, md } = require('vuetify/iconsets/md');
require('vuetify/styles');
require('../styles/app.css');
const lib = require('./lib');
const store = require('./lib/store');
const router = createRouter(lib.router);
sync(store, router);
const App = createApp(app);
const vuetify = createVuetify({
components,
directives,
theme: {
themes: {
light: {
colors: {
primary: '#1fbcd3',
accent: '#ffbb00',
secondary: '#3157d5',
info: '#0D47A1',
warning: '#ffba21',
error: '#a71000',
success: '#1ddf48',
anchor: '#1fbcd3',
},
},
},
},
icons: {
defaultSet: 'md',
aliases,
sets: {
md,
},
},
defaults: {
VCard: {
VCardActions: {
VBtn: { class: 'font-weight-bold' },
},
},
VProgressLinear: {
height: 7,
color: 'primary',
},
},
});
App.use(vuetify);
App.use(store);
const idlePlugin = {
install() {
const idle = new Idle({
idle: 45 * 60 * 1000,
events: ['mousemove', 'keydown', 'mousedown', 'touchstart'],
onIdle() {
window.alert('Sorry, you are being logged out for being idle. Please log back in');
store.dispatch('user/logout');
window.location = store.state.info._links.DesignerLogin.href;
},
keepTracking: true,
startAtIdle: false,
});
idle.start();
},
};
App.use(idlePlugin);
App.use(router);
router.replace('/loading');
store.state.modal = App.$modal;
router.isReady().then(App.mount('#App')).catch((error) => {
console.log(error);
});
================================================
FILE: source/website/js/admin.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-app
v-navigation-drawer(temporary v-model="drawer" width="300")
v-toolbar(flat)
v-list
v-list-item
v-list-item-title(style="font-size: 20px !important; font-weight: 500") Tools
v-divider
v-list(density="compact" lines="three" v-model:opened="open")
v-list-item(v-for="(page,key) in pages" :key="key"
@click="drawer=false"
:href="page.href"
:id="'page-link-'+page.id"
:target="page.target || '_self'")
template(v-slot:prepend)
v-icon.menu-icons(color="primary" :icon="page.icon")
v-list-item-title {{page.title}}
v-list-item-subtitle {{page.subTitle}}
v-divider
v-list-group(
value="QnaHelp")
template(#activator="{ props }")
v-list-item( v-bind="props")
v-list-item-title QnABot Help
template(#prepend)
v-icon(
icon="info"
color="primary"
)
v-list-item
v-list-item-title Version: {{Version}}
v-list-item-title BuildDate: {{BuildDate}}
v-list-item
v-list-item-title
a(href="https://amazon.com/qnabot" target="_blank") General Instructions / QnABot Blog Post
v-list-item-title
a(href="https://qnabot.workshop.aws/" target="_blank") QnABot Workshop
v-list-item-title
a(href="https://aws.amazon.com/blogs/machine-learning/creating-virtual-guided-navigation-using-a-question-and-answer-bot-with-amazon-lex-and-amazon-alexa/" target="_blank") Guided Navigation using QnABot
v-list-item-title
a(href="https://aws.amazon.com/blogs/machine-learning/create-a-questionnaire-bot-with-amazon-lex-and-amazon-alexa/" target="_blank") Create a questionnaire using QnABot
v-list-item-title
a(href="https://aws.amazon.com/blogs/machine-learning/delight-your-customers-with-great-conversational-experiences-via-qnabot-a-generative-ai-chatbot/" target="_blank") Delight your customers with great conversational experiences via QnABot, a generative AI chatbot
v-app-bar()
v-app-bar-nav-icon.text-primary(id="nav-open" @click.stop="drawer = !drawer")
v-app-bar-title
v-breadcrumbs()
v-breadcrumbs-item.text-primary(href='#/edit') {{$store.state.info.StackName}}:{{$store.state.user.name}}
v-breadcrumbs-divider
v-breadcrumbs-item(disabled) {{page}}
v-spacer
v-toolbar-items
v-btn.text-primary(flat
id="logout-button"
@click="logout"
v-if="login") LogOut
v-container(fluid id="workspace")
v-row
v-col
router-view
v-footer
================================================
FILE: source/website/js/browser-check.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const bowser = require('bowser');
document.addEventListener('DOMContentLoaded', () => {
if (!bowser.chrome && !bowser.firefox) {
alert('Warning: Unsupported Browser, please use Chrome or Firefox');
}
});
================================================
FILE: source/website/js/capability/util.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.getUserAgentString = function (version, capability) {
const userAgent = [[`AWSSOLUTION/SO0189/v${version}`], [`AWSSOLUTION-CAPABILITY/SO0189-${capability}/v${version}`]];
return userAgent;
}
================================================
FILE: source/website/js/client.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
import app from './client.vue';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/material-icons';
const { createApp } = require('vue');
const { aliases, md } = require('vuetify/iconsets/md');
const components = require('vuetify/components');
const directives = require('vuetify/directives');
const { createVuetify } = require('vuetify');
const { createStore } = require('vuex');
require('vuetify/styles');
const axios = require('axios');
require('aws-lex-web-ui/dist/lex-web-ui.min.css');
const Auth = require('./lib/client-auth');
let store = null;
let authConfig = null;
const config = {
cognito: {},
lex: {
initialText: 'Ask a Question',
initialSpeechInstruction: '',
reInitSessionAttributesOnRestart: false,
},
ui: {
pageTitle: 'QnABot Client',
toolbarColor: 'cyan',
toolbarTitle: 'QnABot',
toolbarLogo: null,
pushInitialTextOnRestart: false,
AllowSuperDangerousHTMLInMessage: true,
showDialogStateIcon: false,
shouldDisplayResponseCardTitle: false,
positiveFeedbackIntent: 'Thumbs up',
negativeFeedbackIntent: 'Thumbs down',
helpIntent: 'Help',
messageMenu: true,
},
recorder: {},
};
const checkExpiringSessionPlugin = {
install() {
// Compute the session timeout (in milliseconds) using the
// difference between the current timestamp and the expiration timestamp
const sessionTimeout = authConfig.credentials.expiration - Date.now();
if (sessionTimeout > 0) {
setTimeout(() => {
// Note: This is a workaround for the fact that the underlying
// lex-web-ui library does not currently support refreshing
// the session token.
// Tell the user to manually start a new session when the current
// session has expired.
store.dispatch(
'pushErrorMessage',
'Your session has expired. Please start a new session.',
);
}, sessionTimeout);
}
},
};
document.addEventListener('DOMContentLoaded', () => {
const Config = Promise.resolve(axios.head(window.location.href))
.then((result) => {
const stage = result.headers['api-stage'];
return Promise.resolve(axios.get(`/${stage}`)).then((x) => x.data);
})
.then((result) => {
config.cognito.poolId = result.PoolId;
config.cognito.appUserPoolName = result.UserPool;
config.lex.botName = result.BotName;
config.lex.botAlias = result.BotVersion;
config.lex.v2BotId = result.v2BotId;
config.lex.v2BotAliasId = result.v2BotAliasId;
config.lex.v2BotLocaleId = result.v2BotLocaleId;
if (result?.StreamingWebSocketEndpoint) {
config.lex.allowStreamingResponses = true
config.lex.streamingWebSocketEndpoint = result.StreamingWebSocketEndpoint
}
return config;
});
Promise.all([
Config,
Auth(),
])
.then((results) => {
const configResult = results[0];
const auth = results[1];
const LexWebUi = require('aws-lex-web-ui/dist/lex-web-ui.min.js');
const App = createApp(app);
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'md',
aliases,
sets: {
md,
},
},
});
App.use(vuetify);
store = createStore(LexWebUi.Store);
App.use(store);
if (auth.username) {
config.ui.toolbarTitle += ` [${auth.username}]`;
}
config.lex.sessionAttributes = {
idtokenjwt: auth.idtoken,
};
authConfig = auth.config;
App.use(LexWebUi.Plugin, {
config: configResult,
awsConfig: auth.config,
lexRuntimeClient: auth.lexV1,
LexRuntimeV2Client: auth.lexV2,
pollyClient: auth.polly,
});
App.use(checkExpiringSessionPlugin);
App.mount('#App');
});
});
================================================
FILE: source/website/js/client.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
div(id="qna-client")
lex-web-ui
================================================
FILE: source/website/js/components/alexa/index.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container
v-col
v-card
v-card-title
h3 Alexa Instructions
v-card-text(class="pa-0")
v-stepper(
v-model="stepNumber"
class="elevation-0"
:items="steps"
)
template(#item.1)
v-card(flat)
v-card-title(class="text-center") {{ steps[0].title }}
v-card-text(v-html="steps[0].text")
img(
v-if="steps[0].image"
:src="steps[0].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.2)
v-card(flat)
v-card-title(class="text-center") {{ steps[1].title }}
v-card-text(v-html="steps[1].text")
img(
v-if="steps[1].image"
:src="steps[1].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.3)
v-card(flat)
v-card-title(class="text-center") {{ steps[2].title }}
v-card-text(v-html="steps[2].text")
v-card-actions
img(
v-if="steps[2].image"
:src="steps[2].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.4)
v-card(flat)
v-card-title(class="text-center") {{ steps[3].title }}
v-card-text(v-html="steps[3].text")
img(
v-if="steps[3].image"
:src="steps[3].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.5)
v-card(flat)
v-card-title(class="text-center") {{ steps[4].title }}
v-card-text(v-html="steps[4].text")
img(
v-if="steps[4].image"
:src="steps[4].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.6)
v-card(flat)
v-card-title(class="text-center") {{ steps[5].title }}
v-card-text(v-html="steps[5].text")
v-btn(
:id="steps[5].buttons[0].id"
:loading="steps[5].buttons[0].loading"
@click="copy(steps[5].buttons[0])"
) {{ steps[5].buttons[0].text }}
br
br
img(
v-if="steps[5].image"
:src="steps[5].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.7)
v-card(flat)
v-card-title(class="text-center") {{ steps[6].title }}
v-card-text(v-html="steps[6].text")
v-btn(
:id="steps[6].buttons[0].id"
:loading="steps[6].buttons[0].loading"
@click="copy(steps[6].buttons[0])"
) {{ steps[6].buttons[0].text }}
br
br
img(
v-if="steps[6].image"
:src="steps[6].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.8)
v-card(flat)
v-card-title(class="text-center") {{ steps[7].title }}
v-card-text(
class="text-wrap"
v-html="steps[7].text"
)
img(
v-if="steps[7].image"
:src="steps[7].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
================================================
FILE: source/website/js/components/alexa/steps.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = [
{
title: 'Sign-in',
text: `
- Create an Amazon developer account at [Amazon Developer Console](https://developer.amazon.com/home.html)
- Login to the Developer Console and open the "Alexa Skills Kit"
`,
image: '../images/alexa_sign-in.png',
},
{
title: 'Create',
text: `
- Click on the "Create Skill" button
- Follow the instructions to create a skill
`,
image: '../images/alexa_create-skill.png',
},
{
title: 'Model',
text: `
- On the "Experience, Model, Hosting service" tab select:
- "Other" for type of experience
- "Custom" for model type
- "Provision your own" for Hosting services.
`,
image: '../images/alexa_select.png',
},
{
title: 'Template',
text: `
- On the "Template" tab, select "Start from Scratch".
`,
image: '../images/alexa_templates.png',
},
{
title: 'Review',
text: `
- Review the skill which should match the image below and click "Create Skill".
`,
image: '../images/alexa_review.png',
},
{
title: 'Skill Lambda',
text: `
- Click "COPY LAMBDA ARN" on this page and paste in the "Endpoint" page under "Default Region".
- Click "Save".
`,
image: '../images/alexa_lambda-config.png',
buttons: [
{
text: 'COPY LAMBDA ARN',
id: 'LambdaArn',
loading: false,
},
],
},
{
title: 'Schema',
text: `
- Click "COPY SCHEMA" on this page and paste in the "Intents" > "JSON Editor" page.
- Edit the invocation as needed and take note of it. This is the name that you will use to invoke QnABot through Alexa.
- Click "Build Skill".
`,
image: '../images/alexa_schema-config.png',
buttons: [
{
text: 'COPY SCHEMA',
id: 'Schema',
loading: false,
},
],
},
{
title: 'Test',
image: '../images/alexa_enable.png',
text: `
Congratulations! Your QnABot skill is now ready to be used.
Enable testing by selecting the "Test" tab and test your new skill.
### Alexa Device
To access your unpublished skill, register your Alexa device to the same account as your Amazon Developer account.
If you have a device that is not registered to the right account, you can re-register it by following these directions: Registering an Alexa-enabled Device for Testing
Ask questions in the form: *"Alexa, ask q and a, How do I use Q and A Bot?"* (Assuming your device wake word is 'Alexa' and invocationName is 'q and a'). Alternatively, you could use the invocationName 'q and a' to initiate and then ask questions.
Publish your skill if you want to make it available for others to use from their own Amazon accounts.
`,
},
];
================================================
FILE: source/website/js/components/connect/index.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container
v-col
v-card
v-card-title
h3 Connect Instructions
v-card-text(class="pa-0")
v-stepper(
v-model="stepNumber"
class="elevation-0"
:items="steps"
)
template(#item.1)
v-card(flat)
v-card-title(class="text-center") {{ steps[0].title }}
v-card-text(v-html="steps[0].text")
img(
v-if="steps[0].image"
:src="steps[0].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.2)
v-card(flat)
v-card-title(class="text-center") {{ steps[1].title }}
v-card-text(v-html="steps[1].text")
img(
v-if="steps[1].image"
:src="steps[1].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.3)
v-card(flat)
v-card-title(class="text-center") {{ steps[2].title }}
v-card-text(v-html="steps[2].text")
v-card-actions
img(
v-if="steps[2].image"
:src="steps[2].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.4)
v-card(flat)
v-card-title(class="text-center") {{ steps[3].title }}
v-card-text(v-html="steps[3].text")
v-btn(
:id="steps[3].buttons[0].id"
:loading="steps[3].buttons[0].loading"
@click="copy(steps[3].buttons[0])"
) {{ steps[3].buttons[0].text }}
img(
v-if="steps[3].image"
:src="steps[3].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.5)
v-card(flat)
v-card-title(class="text-center") {{ steps[4].title }}
v-card-text(v-html="steps[4].text")
img(
v-if="steps[4].image"
:src="steps[4].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.6)
v-card(flat)
v-card-title(class="text-center") {{ steps[5].title }}
v-card-text(v-html="steps[5].text")
v-btn(
:id="steps[5].buttons[0].id"
:loading="steps[5].buttons[0].loading"
@click="importQuestions(steps[5].buttons[0])"
) {{ steps[5].buttons[0].text }}
img(
v-if="steps[5].image"
:src="steps[5].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
================================================
FILE: source/website/js/components/connect/steps.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = [
{
title: 'Provision a Connect Instance',
text: `
Start by completing all 3 steps below to setup your Amazon Connect instance:
1. Launch Amazon Connect
2. Create an instance
3. Claim a phone number
`,
image: '../images/wizard-1.png',
}, {
title: 'Add QnABot to Contact Flows',
text: `
Now we must make the QnABot accessible to our new call center. Open the Amazon Connect console, and follow the steps below:
1. Choose the Instance Alias you created
2. Select Contact flows
3. Select your bot in the Bot drop down
4. Choose + Add Lex Bot
`,
image: '../images/wizard-2.png',
}, {
title: 'Create Contact Flows',
text: `
1. On the same page, choose Overview on the Left menu
2. Choose the Login URL. It will take you the Amazon Connect Administration App
3. In the Routing menu on the left, choose Contact flows
4. On Contact Flow screen choose Create contact flow
`,
image: '../images/wizard-3.png',
},
{
title: 'Import Contact Flow',
text: `
To begin this step first choose DOWNLOAD CONTACT FLOW below. It will download a JSON contact flow file for QnABot
1. Go back to Amazon Connect Administration App, choose the dropdown on the top right and choose Import Flow
2. Choose the contactflow.json file, that you downloaded from step 1, and choose Import
3. Enter a new name for your contact flow
4. Choose Save
5. Choose Publish
`,
image: '../images/wizard-4.png',
buttons: [{
text: 'DOWNLOAD CONTACT FLOW',
id: 'DownloadContactFlow',
loading: false,
}],
},
{
title: 'Add a Phone Number',
text: `
1. In the Routing menu on the left, choose Phone numbers
2. Choose the Phone Number created on the first step
3. In the Contact Flow / IVR dropdown, select the Contact Flow you created, and choose Save
`,
image: '../images/wizard-5.png',
},
{
title: 'Adding questions and Testing',
buttons: [{
text: 'IMPORT SAMPLE QUESTIONS AND ANSWERS',
id: 'ImportQuestions',
loading: false,
}],
image: '../images/wizard-6.png',
text: `
1. Choose IMPORT SAMPLE QUESTIONS AND ANSWERS below, it can take up to 2 minutes to finish this process.
2. When Status is Complete, enable the new interruptable reponse feature: (i) From the Designer Tools menu (☰) choose Settings, (ii) set CONNECT_ENABLE_VOICE_RESPONSE_INTERRUPT to true, and (iii) save changes.
3. You are ready to try your Bot! Call your new contact center phone number and try some of the questions below.
For more information see our blog post Build an AI powered agent for Amazon Connect using AWS QnABot
`,
},
];
================================================
FILE: source/website/js/components/customTranslate.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container(id="page-import")
v-col
v-card
v-card-title.text-h4.pa-6 Import Translate Custom Terminologies
v-card-text.pb-0.pl-6.pr-6
h3 For more information about Amazon Translate custom terminologies, see here
v-card-text(v-if="!IsCustomTerminologyEnabled")
p Set ENABLE_CUSTOM_TERMINOLOGY to true in settings to enable the use of terminology files for Amazon Translate
v-card-text.pl-6.pr-6(v-if="IsCustomTerminologyEnabled")
em {{ importWarning }}
br
br
h3 From File
br
div.ml-4.mb-2
input(
id="upload-file"
ref="file"
type="file"
name="file"
@change="Getfile"
)
p {{ uploadStatus }}
br
h4 Description:
v-textarea(
v-model="description"
placeholder="Give a description for your file."
variant="outlined"
color="cyan"
)
v-card.pa-4(
v-if="jobs.length>0 && IsCustomTerminologyEnabled"
id="import-jobs"
)
v-card-title Installed Translate Custom Terminologies
v-card-text
v-table
thead
tr
th(style="text-align:left") Name
th(style="text-align:left") Description
th(style="text-align:left") Source Language
th(style="text-align:left") Target Languages
th(style="text-align:left") Number Of Terms
tbody
tr(
v-for="(job,index) in jobs"
:key="index"
)
td {{ job.Name }}
td {{ job.Description }}
td {{ job.SourceLanguage }}
td {{ job.TargetLanguageCodes.join() }}
td {{ job.TermCount }}
================================================
FILE: source/website/js/components/designer/add.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
// To Do L 83
span(class="wrapper")
v-dialog(
v-model="loading"
persistent
max-width="60%"
)
v-card
v-card-title Creating {{ data.qid }}
v-card-text.my-3
v-list-subheader.text-error(
v-if="error"
id="add-error"
) {{ error }}
v-list-subheader.text-success(
v-if="success"
id="add-success"
) {{ success }}
v-progress-linear(
v-if="!error && !success"
indeterminate
)
v-card-actions
v-spacer
v-btn.font-weight-bold(
id="add-close"
flat
@click="cancel"
) close
v-dialog(
ref="dialog"
v-model="dialog"
persistent
max-width="60%"
)
template(v-slot:activator="{ props }")
v-btn.ma-2(
id="add-question-btn"
v-bind="props"
@click="reset"
) Add
v-card(id="add-question-form")
v-card-title
.text-h5 {{ title }}
v-card-text.pb-0
.text-h6 Document Type
v-radio-group(
v-model="type"
inline
color="accent"
)
v-radio.mr-8(
v-for="t in types"
:key="t"
:label="t"
:value="t"
)
v-card-text.pt-0
v-form(v-if="dialog")
schema-input(
ref="requiredInput"
v-model="data[type]"
v-model:valid="valid.required"
:schema="schema"
:pick="required"
:path="type"
)
v-expansion-panels
v-expansion-panel.mt-3(elevation="0" )
v-expansion-panel-title Advanced
v-expansion-panel-text
schema-input(
ref="optionalInput"
v-model="data[type]"
v-model:valid="valid.optional"
:schema="schema"
:omit="schema.required"
:path="type"
)
small * indicates required field
v-list-subheader.text-error(v-if="error") {{ error }}
v-card-actions
v-spacer
v-btn(
id="add-question-cancel"
@click="cancel"
) Cancel
v-btn(
id="add-question-submit"
:disabled="!valid"
@click="add"
) Create
================================================
FILE: source/website/js/components/designer/addSetting.vue
================================================
Click Me
Privacy Policy
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
I accept
================================================
FILE: source/website/js/components/designer/alexa.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-dialog(
v-model="dialog"
persistent
max-width="50%"
)
template(#activator="{ props }")
v-btn(
block
variant="text"
v-bind="props"
) Alexa Update
v-card(id="alexa-modal")
v-card-title
.text-h5 Re-configure Alexa
v-card-text
p You only need to update the schema of your alexa skill.
v-card-actions
v-btn(
v-if="!ready"
:loading="loading"
@click="download"
variant="elevated"
) Copy Schema
v-btn(
v-if="ready"
:loading="loading"
variant="elevated"
@click="copy"
) Copy Schema
input(
id="alexa-schema"
style="display:none"
type="text"
:value="text"
)
v-card-actions
v-spacer
v-btn(@click="dialog = false") Close
================================================
FILE: source/website/js/components/designer/delete.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span(class="wrapper")
v-dialog(v-model="loading" persistent id="delete-loading")
v-card
v-card-title Deleting
v-card-text(v-if="!selectAll")
ul
li(v-for="id in ids") {{id}}
v-card-text
v-list-subheader.text-error(v-if='error' id="delete-error") {{error}}
v-list-subheader.text-success(v-if='success' id="delete-success") {{success}}
v-progress-linear(v-if='!error && !success' indeterminate)
v-card-actions
v-spacer
v-btn(@click='cancel' flat) close
v-dialog(persistent v-model='dialog' max-width='60%')
template(v-slot:activator="{ props }")
v-btn(variant="text" v-bind="props" icon="delete")
v-card(title="Delete Selection")
v-card-text
span(v-if="!selectAll")
p Are you sure you want to delete the following QnAs:
ul.my-3
li(v-for="qa in QAs") {{qa.qid}}
span(v-if="selectAll && !filter")
p Are you sure you want to delete all QnAs
span(v-if="selectAll && filter")
p Are you sure you want to delete all QnAs with prefix:
p {{filter}}
v-card-actions
v-spacer
v-btn(@click='cancel') Cancel
v-btn(@click="rm" id="confirm-delete") Delete
================================================
FILE: source/website/js/components/designer/display.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span
span(v-if="schema.type==='string' && !empty")
v-container.pa-0.fluid
v-row
v-col
.text-h6 {{ schema.title }}
span.pl-3(:data-path="path") {{ modelValue }}
span(v-if="schema.type==='number' && !empty")
v-container.pa-0.fluid
v-row
v-col
.text-h6 {{ schema.title }}
span.pl-3( :data-path="path" ) {{modelValue}}
span(v-if="schema.type==='array' && !empty")
v-container.fluid.pa-0
v-row
v-col
display(
v-for="(item,index) in modelValue"
:schema="schema.items"
:modelValue="item"
:path="path+'['+index+']'"
)
span(v-if="schema.type==='object' && !empty")
v-container.fluid
v-row
.text-h6 {{ schema.title }}
template(v-for="(property,key) in schema.properties")
v-col(v-if="modelValue[key]")
display(
:path="path+'.'+key"
:name="key"
column
:schema="schema.properties[key]"
:modelValue="modelValue[key]"
)
================================================
FILE: source/website/js/components/designer/edit.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span(class="wrapper")
v-dialog(
v-model="loading"
persistent
max-width="60%"
)
v-card(id="edit-loading" title="Updating")
v-card-text.my-3
v-list-subheader.text-error(
v-if="error"
id="edit-error"
) {{ error }}
v-list-subheader.text-success(
v-if="success"
id="edit-success"
) {{ success }}
v-progress-linear(
v-if="!error && !success"
indeterminate
)
v-card-actions
v-spacer
v-btn(
v-if="error"
id="edit-close"
flat
@click="cancel"
) close
v-btn(
v-if="success"
id="edit-close"
flat
@click="close"
) close
v-dialog(
v-model="dialog"
max-width="80%"
)
template(v-slot:activator="{ props }")
v-btn(
v-if="!label"
v-bind="props"
variant="text"
icon="edit"
@click="refresh"
)
v-btn(
v-if="label"
v-bind="props"
@click="refresh"
) {{ label }}
v-card(id="edit-form")
v-card-title(primary-title)
.text-h5 Update: {{ data.qid }}
v-card-text
v-form
schema-input(
v-if="dialog"
v-model="tmp"
v-model:valid="valid"
:schema="schema"
:pick="required"
:path="type"
)
v-expansion-panels
v-expansion-panel.mt-3(elevation="0" style="display:block")
v-expansion-panel-title Advanced
v-expansion-panel-text
schema-input(
v-model="tmp"
v-model:valid="valid"
:schema="schema"
:omit="required"
:path="type"
)
small * indicates required field
v-list-subheader.text-error(v-if="error") {{ error }}
v-card-actions
v-spacer
v-btn(
id="edit-cancel"
@click="cancel"
) Cancel
v-btn(
id="edit-submit"
:disabled="!valid"
@click="update"
) Update
================================================
FILE: source/website/js/components/designer/empty.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
function empty(input) {
if (input.type === 'string') {
return '';
}
if (input.type === 'boolean') {
return false;
}
if (input.type === 'array') {
return [empty(input.items)];
}
if (input.type === 'object') {
const out = {};
Object.keys(input.properties || {}).forEach((key) => (out[key] = empty(input.properties[key])));
return out;
}
}
module.exports = empty;
================================================
FILE: source/website/js/components/designer/event-bus.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vue = require('vue');
export const EventBus = Vue.createApp();
================================================
FILE: source/website/js/components/designer/index.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-card.root-card
v-card-title.pa-0.bg-cyan
v-layout(row)
v-tabs.tabs__item(
v-model="active"
slider-color="accent"
)
v-tab.text-h6(
id="questions-tab"
ripple
value="questions"
) QUESTIONS
v-tab.text-h6(
id="test-tab"
ripple
value="test"
) TEST
v-tab.text-h6(
id="testAll-tab"
ripple
value="testAll"
) TEST ALL
v-spacer
v-menu(
location="bottom"
)
template(#activator="{ props }")
v-btn.icon-button.text-white(
id="edit-sub-menu"
v-bind="props"
icon
)
v-icon more_vert
v-list
v-list-item
alexa
v-list-item
build
v-list-item
sync
span
questions(
v-if="active==='questions'"
@filter="get()"
@refresh="refresh()"
)
test(v-if="active==='test'")
testAll(v-if="active==='testAll'")
v-data-table-server(
v-if="active!=='testAll'"
v-model:items-per-page="itemsPerPage"
v-model:expanded="expanded"
:headers="headers"
:items="QAs"
:search="search"
:items-length="total"
:items-per-page-options="perpage"
:sort-by="sortBy"
:loading="loading"
item-value="qid"
color="primary"
must-sort
@update:options="loadItems"
)
template(#headers="{columns, getSortIcon, toggleSort}")
tr
th.shrink.text-h6(v-if="active==='test'") score
template(
v-for="column in columns"
:key="column.key")
th.shrink(
v-if="active==='questions' && column.title==='SelectAll'"
id="select-all"
)
v-checkbox(
v-model="selectAll"
:indeterminate="QAs.length===0"
tabindex="-1"
color="primary"
@change="toggleSelectAll"
)
th.text-xs-left.text-h6(
v-if="column.title !=='SelectAll' && column.title !=='RowOptions'"
:key="column.title"
:class="['column', column.sortable ? 'sortable' : '']")
span(
class="mr-2 cursor-pointer"
@click="column.sortable && toggleSort(column)"
) {{ column.title }}
v-icon(
v-if="active==='questions' && column.sortable"
size="small"
:icon="getSortIcon(column)"
)
th.flex-grow-1.pa-0(v-if="column.title ==='RowOptions'")
span(
v-if="selectAll | selectedMultiple"
id="delete-all"
)
delete(
:select-all="selectAll"
:selected="selected"
@handle-delete="handleDelete"
)
template(#item="props")
tr(
:id="'qa-'+props.item.qid"
@click="props.toggleExpand(props.internalItem)"
)
td.shrink(
v-if="active==='questions'"
@click.stop=""
)
v-checkbox(
:id="'qa-'+props.item.qid+'-select'"
v-model="props.item.select"
color="primary"
tabindex="-1"
@change="checkSelect"
)
td.text-xs-left.shrink.primary--text.text-h6(
v-if="active==='test'"
) {{ props.item._score || '-' }}
td.text-xs-left.shrink.text-h6
b(:id="props.item.qid") {{ props.item.qid }}
td.text-xs-left.text-h6.font-weight-regular {{ props.item.type || 'qna' }}
td.text-xs-left.text-h6.font-weight-regular {{ (props.item.q && props.item.q[0]) ? props.item.q[0] : (props.item.question ? props.item.question: '') }}
td.flex-grow-1.pa-0.pr-1
edit(
:id="'qa-'+props.item.qid+'-edit'"
v-model:data="props.item"
@filter="get()"
@click.stop=""
)
delete(
:id="'qa-'+props.item.qid+'-delete'"
:data="props.item"
@handle-delete="handleDelete"
)
template(#expanded-row="{ columns, item }")
tr
td(:colspan="columns.length+2")
qa(:data="item")
v-dialog(v-model="deleteLoading" persistent id="delete-loading" max-width='60%')
v-card(title="Deleting")
v-card-text(v-if="!selectAll")
ul.my-3
li(v-for="id in deleteIds") {{id}}
v-card-text
v-list-subheader.text-error(v-if='deleteError' id="delete-error") {{deleteError}}
v-list-subheader.text-success(v-if='deleteSuccess' id="delete-success") {{deleteSuccess}}
v-progress-linear(v-if='!deleteError && !deleteSuccess' indeterminate)
v-card-actions
v-spacer
v-btn.font-weight-bold(@click='deleteClose' flat) close
================================================
FILE: source/website/js/components/designer/input.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
div.input
v-text-field(
v-if="(schema.type==='string' || schema.type==='text') && (!schema.maxLength || schema.maxLength <='5001')"
:id="id"
v-model="local"
:hint="schema.description"
persistent-hint
:required="required"
:rules="[rules.required,rules.schema,rules.maxLength,rules.noSpace]"
:data-vv-name="id"
:data-path="path"
auto-grow
variant="underlined"
color="primary"
:counter="schema.maxLength"
persistent-counter
validate-on="input"
@update:error="setValid"
)
template(#label)
span(v-if="required") {{ schema.title }}*
span(v-if="!required") {{ schema.title }}
v-textarea(
v-if="(schema.type==='string' || schema.type==='text') && schema.maxLength>'5001'"
:id="id"
v-model="local"
:hint="schema.description"
persistent-hint
:required="required"
:rules="[rules.required,rules.schema,rules.maxLength]"
:data-vv-name="id"
:textarea="schema.maxLength>'5001'"
:data-path="path"
auto-grow
variant="outlined"
color="primary"
:counter="schema.maxLength"
persistent-counter
validate-on="input"
@update:error="setValid"
)
template(#label)
span(v-if="required") {{ schema.title }}*
span(v-if="!required") {{ schema.title }}
v-checkbox(
v-if="schema.type==='boolean'"
v-model="local"
:label="schema.title"
:hint="schema.description"
persistent-hint
:required="required"
:rules="[rules.required,rules.schema]"
:data-path="path"
@update:error="setValid"
)
div(v-if="schema.type==='array'")
.text-subtitle-1 {{ schema.title }}
span.text-body-2 {{ schema.description }}
ul.pl-3
li(
v-for="(item,index) in modelValue"
:key="index"
)
schema-input(
:ref="index"
v-model="modelValue[index]"
:schema="schema.items"
:index="index"
:required="index===0"
:name="name"
:path="path+'['+index+']'"
style="display:inline-block;width:80%;"
@update:valid="isValid"
)
v-btn.delete(
:id="path+'-remove-'+index"
icon="delete"
tabindex="-1"
variant="text"
@click="remove(index)"
)
v-btn.mb-3(
:id="path+'-add'"
tabindex="-1"
@click="add"
) Add {{ singularTitle }}
div(v-if="schema.type==='object'")
.text-subtitle-1 {{ schema.title }}
span.text-body-2(v-if="schema.description") {{ schema.description }}
ul
li.py-2(
v-for="(property,index) in properties"
:key="index"
)
schema-input(
:ref="property.name"
v-model="modelValue[property.name]"
:required="ifRequired(property.name)"
:schema="property"
:name="property.name"
:path="path+'.'+property.name"
style="margin-left:5%;"
@update:valid="isValid"
)
================================================
FILE: source/website/js/components/designer/menu-questions.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container(fluid)
v-row.mx-5(align="center"
align-content="center")
v-col.pr-0(cols="6")
v-text-field(
id="filter"
v-model="$store.state.data.filter"
name="filter"
label="Filter items by ID prefix"
variant="underlined"
color="primary"
clearable
persistent-clear
prepend-inner-icon="search"
@input="filter"
@click:clear="filter"
)
v-col.pl-2()
div(class="d-flex flex-row")
v-btn(
class="ma-2 refresh"
@click="refresh"
)
span Refresh
add
v-dialog(v-model="error")
v-card(id="error-modal")
v-card-title(primary-title) Error Loading Content
v-card-text
v-list-subheader.text-error(v-if='error' id="add-error") {{errorMsg}}
v-card-actions
v-spacer
v-btn.lighten-3(@click="error=false;errorMsg='';" :class="{ teal: success}" ) close
================================================
FILE: source/website/js/components/designer/menu-test.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container(fluid)
v-row
v-col
v-text-field(
id="query"
v-model="query"
name="query"
label="Type your question here"
clearable
color="primary"
variant="underlined"
persistent-clear
@keyup.enter="simulate"
)
v-col
v-select(
v-model="score_on"
label="Match on:"
:items="['qna item questions', 'qna item answer', 'text item passage']"
bg-color="none"
color="primary"
density="comfortable"
)
v-row
v-col
v-text-field(
name="topic"
label="(optional) Topic context"
id="topic"
v-model="topic"
@keyup.enter="simulate"
clearable
color="primary"
variant="underlined"
persistent-clear
)
v-col
v-btn(
id="query-test"
@click="simulate"
) Search
v-row
v-col(cols="6")
v-text-field(
name="client_filter"
label="(optional) Client filter context"
id="client_filter"
v-model="client_filter"
@keyup.enter="simulate"
clearable
color="primary"
variant="underlined"
persistent-clear
)
================================================
FILE: source/website/js/components/designer/menu-testall.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container(fluid style="height: 80vh !important")
v-row
v-col
v-text-field(
id="filename"
v-model="filename"
name="filename"
label="Filename"
clearable
color="primary"
variant="underlined"
persistent-clear
)
v-text-field(
id="filter"
v-model="filter"
name="filter"
label="(optional) filter test all by qid prefix"
clearable
color="primary"
variant="underlined"
persistent-clear
)
v-select(
id="selectedLocale"
v-model="selectedLocale"
label="Locale"
:items="localeIds"
bg-color="none"
color="primary"
density="comfortable"
)
v-btn(
id="testAll"
@click="start"
) Test All
v-row
v-col(v-if="testjobs.length>0")
v-card(id="test-jobs")
v-card-title.text-h5 Tests
v-card-text
v-list
template(v-for="(job,index) in testjobs")
v-list-item.job-content(:id="'test-job-'+job.id" :data-status="job.status")
v-list-item-title {{job.id}}: {{job.status}}
v-list-item-subtitle
v-progress-linear(v-bind:indeterminate="isIndeterminate(job)" v-model="job.progress")
template(#append)
v-btn(icon="delete" variant="text" @click="remove(index)")
v-btn(v-show="job.status==='Completed'" variant="text"
icon="file_download" @click="download(index)" :loading="job.loading")
v-btn(v-show="job.status==='Completed'" variant="text"
icon="open_in_browser" @click="quickview(index)" :loading="job.loading")
v-divider(v-if="index + 1 < testjobs.length")
v-row
v-col(v-if="isModalVisible")
modal(v-bind:table-data="tableData"
v-bind:table-header="tableHeader" @closemodal="closeModal")
================================================
FILE: source/website/js/components/designer/modal.vue
================================================
================================================
FILE: source/website/js/components/designer/qa.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-card(flat class="pa-0")
span(v-show="false" :data-path="data.qid+'-.qid'") {{data.qid}}
display(
:schema="schema"
:path='data.qid+"-"'
row
v-model="topitems"
)
v-divider(v-if="extra")
display(
v-if="extra"
:schema="schema"
:path='data.qid+"-"'
column
v-model="bottomitems"
)
================================================
FILE: source/website/js/components/designer/rebuild.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span(class="wrapper")
v-dialog(
v-model="snackbar"
persistent
)
template(#activator="{ props }")
v-btn(
id="lex-rebuild"
:disabled="loading"
block
variant="text"
v-bind="props"
@click="build"
) Lex Rebuild
v-card(id="lex-loading")
v-card-title Rebuilding : {{status}}
v-card-text
v-list-subheader.text-error(v-if='error' id="lex-error") {{error}}
v-list-subheader.text-success(v-if='success' id="lex-success") Success!
v-list-subheader.text-error(v-if='message' ) {{message}}
v-progress-linear(v-if='!error && !success' indeterminate)
v-card-actions
v-spacer
v-btn(@click='cancel' flat id="lex-close") close
================================================
FILE: source/website/js/components/designer/synckendra.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span(class="wrapper")
v-dialog(
v-model="snackbar"
persistent
)
template(#activator="{ props }")
v-btn(
id="kendra-sync"
:disabled="!(kendraFaqEnabled && !loading)"
block
v-bind="props"
variant="text"
@click="start"
) Sync Kendra FAQ
v-card(id="kendra-syncing")
v-card-title Syncing: {{ request_status }}
v-card-text
v-list-subheader.text-error(
v-if="error"
id="error") {{ error }}
v-list-subheader.text-success(
v-if="success"
id="success") Success!
v-progress-linear(
v-if="!error && !success"
indeterminate
)
v-card-actions
v-spacer
v-btn(id="kendra-close"
flat
@click="cancel"
) Close
================================================
FILE: source/website/js/components/export.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container
v-row
v-col
v-card
v-card-title.text-h4.pa-2 Export
v-card-text.mt-5
v-text-field(name="filename"
label="filename"
:rules="[rules.validJson]"
id="filename" clearable v-model="filename" variant="underlined" color="primary" persistent-clear)
v-text-field(name="filter"
label="(optional) filter export by qid prefix"
id="filter" clearable v-model="filter" variant="underlined" color="primary" persistent-clear)
v-card-actions
v-spacer
v-btn(@click="start()" id="export" variant="elevated") export
v-row
v-col(v-if="exports.length>0")
v-card(id="export-jobs")
v-card-title.text-h5 Exports
v-card-text
v-list
template(v-for="(job,index) in exports" :key="job.id")
v-list-item.job-content(:id="'export-job-'+job.id" :data-status="job.status")
v-list-item-title {{job.id}}: {{job.status}}
v-list-item-subtitle
v-progress-linear(v-model="job.progress")
template(#append)
v-btn(icon="delete" variant="text" @click="remove(index)")
v-btn(v-show="job.status==='Completed'"
variant="text" icon="file_download" @click="download(index)" :loading="job.loading")
v-divider(v-if="index + 1 < exports.length")
================================================
FILE: source/website/js/components/genesys/index.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container
v-col
v-card
v-card-title
h3 Genesys Cloud CX Instructions
v-card-text(class="pa-0")
v-stepper(
v-model="stepNumber"
class="elevation-0"
:items="steps"
)
template(#item.1)
v-card(flat)
v-card-title(class="text-center") {{ steps[0].title }}
v-card-text(v-html="steps[0].text")
img(
v-if="steps[0].image"
:src="steps[0].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.2)
v-card(flat)
v-card-title(class="text-center") {{ steps[1].title }}
v-card-text(v-html="steps[1].text")
img(
v-if="steps[1].image"
:src="steps[1].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.3)
v-card(flat)
v-card-title(class="text-center") {{ steps[2].title }}
v-card-text(v-html="steps[2].text")
img(
v-if="steps[2].image"
:src="steps[2].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.4)
v-card(flat)
v-card-title(class="text-center") {{ steps[3].title }}
v-card-text(v-html="steps[3].text")
v-card-actions
v-btn(
:id="steps[3].buttons[0].id"
:loading="steps[3].buttons[0].loading"
@click="copy(steps[3].buttons[0])"
) {{ steps[3].buttons[0].text }}
template(#item.5)
v-card(flat)
v-card-title(class="text-center") {{ steps[4].title }}
v-card-text(v-html="steps[4].text")
img(
v-if="steps[4].image"
:src="steps[4].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
template(#item.6)
v-card(flat)
v-card-title(class="text-center") {{ steps[5].title }}
v-card-text(v-html="steps[5].text")
img(
v-if="steps[5].image"
:src="steps[5].image"
style="max-width:75%;display:block;margin:auto;"
contain
)
================================================
FILE: source/website/js/components/genesys/steps.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = [
{
title: 'Connecting QnABot on AWS to Genesys Cloud CX',
text: `
Important Note:
While QnABot on AWS provides integration with Genesys Cloud CX, you are responsible for integration testing and ensuring QnABot connectivity to Genesys Cloud works as expected. Please work with a specialist if you have further questions.
For high level steps on how to integrate, please refer to the QnABot Workshop and/or blog post: Build an AI-powered virtual agent for Genesys Cloud using QnABot and Amazon Lex.
`,
},
{
title: 'Configure Genesys Cloud CX Integration',
text: `
Configure LexV2 and Genesys Cloud CX with AppFoundry
Do step 2 through 4 in the Lex V2 quick start guide.
Note: Step 1 is already complete, as QnABot is a Lex bot.
Step 1. SKIP STEP 1, QnABot deploys the LexV2 bot
Step 2. Grant Genesys Cloud CX the permissions to call the Amazon Lex V2 bot
Step 3. Obtain the Amazon Lex V2 integration from Genesys AppFoundry
Step 4. Configure and activate the Lex V2 integration in Genesys Cloud CX
`,
image: '../images/genesys-1.png',
}, {
title: 'Install Archy',
text: `
Install and Configure Archy
Download, install and configure the Genesys Cloud Architect YAML (Archy) processor. All the instructions are on the Genesys Archy developer website,
found here:https://developer.genesys.cloud/devapps/archy/install
High level steps:
1. Download the version of Archy for your operating system
2. Extract or install Archy
3. Run archy setup and configure authentication credentials
`,
image: '../images/genesys-2.png',
},
{
title: 'Download Call Flow',
text: `
Download Genesys Cloud CX Inbound Call Flow
To begin this step, choose DOWNLOAD INBOUND CALL FLOW below. It will download a YAML inbound call flow file for QnABot.
`,
buttons: [{
text: 'DOWNLOAD INBOUND CALL FLOW',
id: 'DownloadInboundCallFlow',
loading: false,
}],
},
{
title: 'Import Call Flow',
text: `
Import Call Flow with Archy
In the terminal, run archy publish --file QnABotFlow.yaml. This will create and publish the call flow, which will then appear in Genesys Architect.
`,
image: '../images/genesys-3.png',
},
{
title: 'Configure Call Routing',
image: '../images/genesys-4.png',
text: `
Configure Call Routing in Genesys Pure Cloud Admin
In Genesys Pure Cloud Admin, configure a call route to route to the newly published QnABot inbound call flow.
`,
},
];
================================================
FILE: source/website/js/components/hooks/codejs.txt
================================================
exports.handler = function (event, context, callback) {
console.log('Input:', JSON.stringify(event, null, 2));
callback(null, event);
};
================================================
FILE: source/website/js/components/hooks/codepy.txt
================================================
import json
def lambda_handler(event,context):
print(json.dumps(event,indent=4))
return event
================================================
FILE: source/website/js/components/hooks/example.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const session = {
previous: JSON.stringify({
qid: 'test.1',
q: 'help',
a: 'ask a question',
}),
};
module.exports = {
req: {
_type: 'LEX',
question: 'help',
session,
_info: {
es: {
address: 'OpenSearch address',
index: 'QnABot index in OpenSearch',
type: 'QnABot type in OpenSearch',
service: {
qid: 'Arn of ES qid lambda',
proxy: 'Arn of ES proxy lambda',
},
},
},
_original: {
currentIntent: {
name: 'intent-name',
slots: {
'slot-name': 'value',
},
confirmationStatus: 'None, Confirmed, or Denied (intent confirmation, if configured)',
},
bot: {
name: 'bot-name',
alias: 'bot-alias',
version: 'bot-version',
},
userId: 'user-id specified in the POST request to Amazon Lex.',
inputTranscript: 'help',
invocationSource: 'FulfillmentCodeHook or DialogCodeHook',
outputDialogMode: 'Text or Voice, based on ContentType request header in runtime API request',
messageVersion: '1.0',
sessionAttributes: session,
},
},
res: {
type: 'plaintext',
message: '',
session: {
key1: 'value1',
key2: 'value2',
},
card: {
send: false,
title: '',
text: '',
url: '',
},
},
};
================================================
FILE: source/website/js/components/hooks/index.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container
v-col
v-card
v-card-title
h3 Lambda Hook Instructions
v-card-text(class="pa-0")
v-stepper(
v-model="stepNumber"
class="elevation-0"
:items="steps"
)
template(#item.1)
v-card(flat)
v-card-title(class="text-center") {{ steps[0].title }}
v-card-text(v-html="steps[0].text")
v-card-actions
v-btn(
:id="steps[0].buttons[0].id"
:loading="steps[0].buttons[0].loading"
@click="copy(steps[0].buttons[0])"
) {{ steps[0].buttons[0].text }}
template(#item.2)
v-card(flat)
v-card-title(class="text-center") {{ steps[1].title }}
v-card-text(v-html="steps[1].text")
v-card-actions
template(
v-for="(button,index) in steps[1].buttons"
:key="index"
)
v-btn(
:id="button.id"
:loading="button.loading"
@click="copy(button)"
) {{ button.text }}
template(#item.3)
v-card(flat)
v-card-title(class="text-center") {{ steps[2].title }}
v-card-text(v-html="steps[2].text")
template(#item.4)
v-card(flat)
v-card-title(class="text-center") {{ steps[3].title }}
v-card-text(v-html="steps[3].text")
================================================
FILE: source/website/js/components/hooks/steps.js
================================================
/* eslint-disable max-len */
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const stringify = require('json-stringify-pretty-compact');
const example = stringify(require('./example'));
const codeJS = require('./codejs.txt');
const codePY = require('./codepy.txt');
module.exports = [{
title: 'Create Lambda Function',
text: `
1. Create a lambda function with a name that starts with "qna-", for example:
> qna-ExtraSpecial
> qna-SecretSauce
> ...
2. Choose a runtime, our examples will use either nodejs or python.
3. Click "Copy Lambda Role" below and paste into Role
4. click "Create Function"
`,
buttons: [{
text: 'Copy Lambda Role',
id: 'Role',
loading: false,
}],
}, {
title: 'Write Code',
text: `
A minimal function would look like this
### Node.js Code
~~~js
${codeJS}
~~~
### Python Code
~~~python
${codePY}
~~~
The event object has two properties
1. \`event.req\` the normalized request object
1. \`event.res\` the normalized response object (edit this to change the response)
The lambda handler must return the modified event object.
### Example Event
~~~json
${example}
~~~
`,
buttons: [{
text: 'Copy node.js Code',
id: 'code-js',
loading: false,
}, {
text: 'Copy python Code',
id: 'code-py',
loading: false,
}, {
text: 'Copy Example Event',
id: 'request',
loading: false,
}],
}, {
title: 'Add/Edit Question',
text: `
For a new or existing question edit the Lambda field to contain the name or ARN of your created lambda function
`,
}, {
title: 'Test Question',
text: `
Ask question in QnAClient to see your new response
`,
},
];
================================================
FILE: source/website/js/components/import.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
span.wrapper
v-dialog(v-model='error' scrollable width='70%')
v-card#error-modal
v-card-title() Error Loading Content
v-card-text
li(v-for='error in errorList') {{ error }}
v-card-actions
v-spacer
v-btn(
@click='error = false; errorList = []; errorMsg = ""; $refs.file.value = []',
:class='{ teal: success }'
) close
v-container#page-import()
v-row
v-col
v-card
v-card-title.text-h4.pa-2 Import
v-card-text
p.mb-2
p.text-h6.mb-2 From File
.ml-4.mb-2
input#upload-file(type='file', name='file', v-on:change='Getfile', ref='file')
p.text-h6.mb-2 From url
.d-flex.ml-4
v-text-field#url(
name='url',
label='Type here to import from url',
clearable,
v-model='url',
variant="underlined",
color="primary",
persistent-clear
)
v-btn#import-url(@click='Geturl', :disabled='url?.length === 0') import
v-row
v-col(v-if='jobs.length > 0')
v-card#import-jobs
v-card-title.text-h5 Import Jobs
v-card-text
v-list
template(v-for='(job, index) in jobs' :key="job.id")
v-list-item(
:id="'import-job-' + job.id"
:data-status="job.status"
)
v-list-item-title.job-content {{ job.id }}: {{ job.status }}
v-list-item-subtitle
v-progress-linear(v-model="job.progress")
template(v-slot:append)
v-btn(icon="delete" variant="text" @click='deleteJob(index)', :loading='job.loading')
v-divider(v-if='index + 1 < jobs.length')
v-row
v-col
v-expansion-panels
v-expansion-panel
v-expansion-panel-title
p#examples-open.text-h5(slot='header') Examples/Extensions
v-expansion-panel-text(eager=true)
v-list(lines="two")
template(v-for='(example, index) in examples')
v-divider
v-list-item
template(v-slot:prepend)
v-tooltip(location="bottom")
template(v-slot:activator="{ props }")
v-icon.pr-3(v-bind="props") info
span.text-subtitle-1 {{ example.text }}
v-list-item-title.text-h6 {{ example.id }}
v-tooltip(location="bottom")
template(v-slot:activator="{ props }")
v-list-item-subtitle(v-bind="props") {{ example.text }}
span.text-subtitle-1 {{ example.text }}
template(v-slot:append)
v-btn.example(
@click='importExample(example.document.href)',
:id='"example-" + example.id'
) Load
================================================
FILE: source/website/js/components/kendraIndex.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container#page-import
v-col
v-card
v-card-title.text-h4.pa-6 Kendra Web Crawling
v-card-text.pb-0.pl-6.pr-6
h3 For more information about Kendra Web Crawling, see here
v-card-text(v-if="!kendraIndexerEnabled").pa-6
h4 To Enable Kendra Web Crawling, following configuration is required.
br
p KendraWebPageIndexId parameter in CloudFormation stack should be set to the ID of the Kendra Index used to store the content of the web pages
p Also the following settings should be configured from QnABot Designer -
p
p ENABLE_KENDRA_WEB_INDEXER - should be set to true
p KENDRA_INDEXER_URLS - a comma separated list of web pages to index
v-card-actions.pa-6.pb-2
v-row.pa-6.pb-0
v-btn#btnKendraStartIndex(
:disabled="status == 'STARTING' || status == 'CREATING' || status == 'UPDATING' || status == 'DELETING' || status == 'SYNCING' || status =='SYNCING_INDEXING' || status == 'STOPPING' || !kendraIndexerEnabled",
variant="tonal"
@click="start"
) Start Crawling
v-card-text.pt-2(v-if="kendraIndexerEnabled == true")
p Current Status: {{ status }}
v-col(v-if="history && history.length > 0")
v-card-title Kendra Crawling History
v-card-text
h3 View Web Crawling Errors in CloudWatch
v-card-text
v-table
thead
tr(style="text-align: left")
th(style="text-align: left") Start Time
th(style="text-align: left") End Time
th(style="text-align: left") Status
th(style="text-align: left") Error Message
th(style="text-align: left") Documents Scanned
th(style="text-align: left") Documents Added
th(style="text-align: left") Documents Modified
th(style="text-align: left") Documents Deleted
th(style="text-align: left") Documents Failed
tbody
tr(
v-for="(job,index) in history"
:key="index"
)
td {{ convertToLocalTime(job.StartTime) }}
td {{ convertToLocalTime(job.EndTime) }}
td {{ job.Status }}
td {{ job.ErrorMessage }}
td {{ job.Metrics.DocumentsScanned }}
td {{ job.Metrics.DocumentsAdded }}
td {{ job.Metrics.DocumentsModified }}
td {{ job.Metrics.DocumentsDeleted }}
td {{ job.Metrics.DocumentsFailed }}
================================================
FILE: source/website/js/components/loading.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-card
v-progress-linear(indeterminate)
================================================
FILE: source/website/js/components/settings.vue
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
v-container()
v-dialog(v-model="showAlert" scrollable width="70%")
v-card(id="error-modal")
v-card-title {{alertTitle}}
v-card-text {{alertMessage}}
v-card-actions
v-spacer
v-btn(@click="showAlert=false;" ) close
v-row
v-col
v-card
v-card-title
h2 Settings
v-card-text
h3 For more information about settings, see here
template(
v-for="(headerSetting,headerIndex) in settingsMap"
:key="headerIndex"
)
v-card-text
br
h2 {{ headerSetting.label }}
v-divider(:thickness="5")
v-expansion-panels(
multiple=true
v-model="headerSetting.openedPanels"
variant="accordion"
)
v-expansion-panel(
v-for="(subgroupSetting,subgroupIndex) in headerSetting.subgroups"
:key="subgroupIndex"
)
v-expansion-panel-title(
:id="subgroupSetting.id"
)
h3 {{ subgroupSetting.label }}
v-expansion-panel-text
v-list(:lines="'three'")
v-list-item(
v-for="(memberSetting,memberIndex) in subgroupSetting.members"
:key="memberIndex"
)
v-select(
v-if="memberSetting.type==='boolean'"
:id="memberSetting.id"
v-model="settingsHolder[memberSetting.id]"
:label="memberSetting.id"
:items="['true', 'false']"
:hint="memberSetting.hint"
:persistent-hint="true"
)
v-text-field(
v-else-if="memberSetting.type==='number'"
:id="memberSetting.id"
v-model.number="settingsHolder[memberSetting.id]"
:label="memberSetting.id"
variant="underlined"
color="primary"
:hint="memberSetting.hint"
:persistent-hint="true"
)
v-select(
v-else-if="memberSetting.type==='enum'"
:id="memberSetting.id"
v-model="settingsHolder[memberSetting.id]"
:label="memberSetting.id"
:items="memberSetting.enums"
:hint="memberSetting.hint"
:persistent-hint="true"
)
v-textarea(
v-else-if="memberSetting.type==='textarea'"
:id="memberSetting.id"
v-model="settingsHolder[memberSetting.id]"
:label="memberSetting.id"
variant="underlined"
color="primary"
:hint="memberSetting.hint"
:persistent-hint="true"
)
v-text-field(
v-else
:id="memberSetting.id"
v-model="settingsHolder[memberSetting.id]"
:label="memberSetting.id"
variant="underlined"
color="primary"
:hint="memberSetting.hint"
:persistent-hint="true"
)
v-card-text
br
h2 Custom Settings
v-divider(:thickness="5")
v-expansion-panels(
v-model="userDefinedPanels"
multiple=true
variant="accordion"
)
v-expansion-panel
v-expansion-panel-title
h3 User-Defined
v-expansion-panel-text
template(
v-for="(parameter,index) in customSettings"
:key="index"
)
v-text-field(
v-if="defaultSettings[index]===undefined"
:id="index"
v-model="settingsHolder[index]"
:label="index"
variant="underlined"
color="primary"
class="mt-6"
)
v-card-text
v-btn.mr-3.my-2(@click="SaveSettings") Save
//- v-btn Add New parameter
v-btn.my-2(@click="resetToDefaults" style="margin-right:80px;") Reset to defaults
v-btn.mr-3.my-2(@click="$refs.fileInput.click()") Import Settings
v-btn.my-2(@click="ExportSettings" style="margin-right:80px;") Export Settings
v-btn.my-2(@click="showAddModal = true") Add New Setting
input(type="file" ref="fileInput" accept="application/json" @change="onFilePicked" style="display: none")
v-dialog(v-model ="showAddModal")
v-card
v-card-title New Setting
v-card-text
v-text-field(label="Name" v-model="newKey" variant="underlined" color="primary")
v-text-field(label="Value" v-model="newValue" variant="underlined" color="primary")
v-card-actions
v-spacer
v-btn(@click="addSetting") Add
v-btn(@click="closeModal") Cancel
================================================
FILE: source/website/js/lib/client-auth.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { fromCognitoIdentityPool } = require('@aws-sdk/credential-providers');
const { CognitoIdentityClient, GetIdCommand, GetCredentialsForIdentityCommand } = require('@aws-sdk/client-cognito-identity');
const { LexRuntimeServiceClient } = require('@aws-sdk/client-lex-runtime-service');
const { LexRuntimeV2Client } = require('@aws-sdk/client-lex-runtime-v2');
const { PollyClient } = require('@aws-sdk/client-polly');
const axios = require('axios');
const _ = require('lodash');
const query = require('query-string');
const jwt = require('jsonwebtoken');
async function axiosPost(axiosMethod, axiosClient, axiosData) {
const tokens = await axios({
method: axiosMethod,
url: `${axiosClient}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: axiosData,
});
return tokens;
}
async function getTokens(response, code) {
const endpoint = response.data._links.CognitoEndpoint.href;
const clientId = response.data.ClientIdClient;
try {
const axiosData = query.stringify({
grant_type: 'authorization_code',
client_id: clientId,
code,
redirect_uri: window.location.origin + window.location.pathname,
});
const tokens = await axiosPost('POST', endpoint, axiosData);
window.sessionStorage.setItem('refresh_token', tokens.data.refresh_token);
return tokens.data.id_token;
} catch (e) {
console.log(e);
const result = window.confirm('Unable to fetch credentials, please log back in. Click Ok to be redirected to the login page.');
if (result) {
window.location.href = response.data._links.ClientLogin.href;
}
}
}
async function refreshTokens(response) {
const refresh_token = window.sessionStorage.getItem('refresh_token');
const endpoint = response.data._links.CognitoEndpoint.href;
const clientId = response.data.ClientIdClient;
try {
const axiosData = query.stringify({
grant_type: 'refresh_token',
client_id: clientId,
refresh_token,
});
const tokens = await axiosPost('POST', endpoint, axiosData);
return tokens.data.id_token;
} catch (e) {
console.log(e);
}
}
async function getIdentityId(region, identityPoolId, Logins) {
const client = new CognitoIdentityClient({ region });
const input = {
IdentityPoolId: identityPoolId,
Logins,
};
const command = new GetIdCommand(input);
try {
const res = await client.send(command);
return res.IdentityId;
} catch (error) {
console.log('Error while retrieving Identity Id:', error);
}
}
async function getAuthCredentials(region, identityId, Logins) {
const client = new CognitoIdentityClient({ region });
const input = {
IdentityId: identityId,
Logins,
};
const command = new GetCredentialsForIdentityCommand(input);
try {
const res = await client.send(command);
const creds = res.Credentials;
const credentials = {
accessKeyId: creds.AccessKeyId,
identityId,
secretAccessKey: creds.SecretKey,
sessionToken: creds.SessionToken,
expiration: creds.Expiration,
};
return credentials;
} catch (error) {
console.log('Error while retrieving Auth Credentials:', error);
}
}
async function getCredentials(region, poolId, login = {}) {
const credentialProvider = fromCognitoIdentityPool({
identityPoolId: poolId,
logins: login,
clientConfig: { region },
});
const credentials = credentialProvider();
return credentials;
}
module.exports = async function () {
const result = await axios.head(window.location.href);
const stage = result.headers['api-stage'];
const response = await axios.get(`/${stage}`);
const info = response.data;
const { region } = info;
let credentials, username, token, identityId, polly, lexV1, lexV2;
const { code } = query.parse(window.location.search);
if (code) {
if (window.sessionStorage.getItem('refresh_token')) {
token = await refreshTokens(response);
}
if (!token) {
token = await getTokens(response, code);
}
const decodedToken = jwt.decode(token);
const Logins = {};
Logins[[
'cognito-idp.',
info.region,
'.amazonaws.com/',
info.UserPool,
].join('')] = token;
identityId = await getIdentityId(region, info.PoolId, Logins);
credentials = await getAuthCredentials(region, identityId, Logins);
const awsConfig = {
region,
credentials,
};
polly = new PollyClient(awsConfig);
lexV1 = new LexRuntimeServiceClient(awsConfig);
lexV2 = new LexRuntimeV2Client(awsConfig);
username = decodedToken['cognito:username'];
} else {
credentials = await getCredentials(region, info.PoolId);
const awsConfig = {
region,
credentials,
};
polly = new PollyClient(awsConfig);
lexV1 = new LexRuntimeServiceClient(awsConfig);
lexV2 = new LexRuntimeV2Client(awsConfig);
}
return {
config: {
region,
credentials,
},
lexV1,
lexV2,
polly,
username,
Login: _.get(info, '_links.ClientLogin.href'),
idtoken: token,
};
};
================================================
FILE: source/website/js/lib/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
exports.router = require('./router.js');
exports.store = require('./store');
================================================
FILE: source/website/js/lib/router.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
/* eslint-disable global-require */
const { createWebHashHistory } = require('vue-router');
module.exports = {
base: '/',
history: createWebHashHistory(),
routes: [
{
path: '/alexa',
name: 'alexa',
component: require('../components/alexa/index.vue').default,
},
{
path: '/connect',
name: 'connect',
component: require('../components/connect/index.vue').default,
},
{
path: '/genesys',
name: 'genesys',
component: require('../components/genesys/index.vue').default,
},
{
path: '/hooks',
name: 'hooks',
component: require('../components/hooks/index.vue').default,
},
{
path: '/import',
name: 'import',
component: require('../components/import.vue').default,
},
{
path: '/customTranslate',
name: 'Import Custom Terminology',
component: require('../components/customTranslate.vue').default,
},
{
path: '/kendraIndex',
name: 'Kendra Web Page Indexing',
component: require('../components/kendraIndex.vue').default,
},
{
path: '/export',
name: 'export',
component: require('../components/export.vue').default,
},
{
path: '/edit',
name: 'edit',
component: require('../components/designer/index.vue').default,
},
{
path: '/loading',
component: require('../components/loading.vue').default,
},
{
path: '/',
component: require('../components/loading.vue').default,
},
{
path: '/settings',
name: 'settings',
component: require('../components/settings.vue').default,
},
],
};
================================================
FILE: source/website/js/lib/store/actions.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const axios = require('axios');
const vue = require('vue');
module.exports = {
async bootstrap(context) {
const result = await Promise.resolve(axios.head(window.location.href));
const stage = result.headers['api-stage'];
const x = await Promise.resolve(axios.get(`/${stage}`));
const assigned = Object.assign(x.data, { stage });
context.commit('info', assigned);
},
};
================================================
FILE: source/website/js/lib/store/api/actions/connect.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
getContactFlow(context, opts) {
return context.dispatch('_request', {
url: context.rootState.info._links.connect.href,
method: 'get',
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/export.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const { DynamoDBClient, ScanCommand } = require('@aws-sdk/client-dynamodb');
const { unmarshall } = require('@aws-sdk/util-dynamodb');
const util = require('./../../../../capability/util');
const EMPTY_SENTINEL = 'EMPTY_STRING_BY_USER';
async function getParameters(context, dynamodb) {
const tableName = context.rootState.info.SettingsTable;
const params = {
TableName: tableName,
FilterExpression: "SettingCategory <> :private",
ExpressionAttributeValues: {
":private": {
"S": "Private"
}
}
};
const custom_settings = {};
const default_settings = {}
let lastEvaluatedKey = null;
do {
if (lastEvaluatedKey) {
params.ExclusiveStartKey = lastEvaluatedKey;
}
try {
const command = new ScanCommand(params);
const response = await dynamodb.send(command);
response.Items.forEach(item => {
const unmarshalledItem = unmarshall(item);
const settingName = unmarshalledItem.SettingName;
const settingValue = unmarshalledItem.SettingValue;
const defaultValue = unmarshalledItem.DefaultValue;
const settingCategory = unmarshalledItem.SettingCategory;
if (settingValue === EMPTY_SENTINEL) {
custom_settings[settingName] = "";
} else if (settingValue !== "") {
custom_settings[settingName] = settingValue;
}
if (settingCategory == "Custom") {
custom_settings[settingName] = settingValue;
}
default_settings[settingName] = defaultValue;
});
lastEvaluatedKey = response.LastEvaluatedKey;
} catch (error) {
console.error('Error scanning DynamoDB table:', error);
throw error;
}
} while (lastEvaluatedKey);
let cloned_default = _.clone(default_settings)
let merged_settings = _.merge(cloned_default, custom_settings)
return [default_settings, custom_settings, merged_settings];
}
async function listSettings(context) {
const credentials = context.rootState.user.credentials;
const dynamodb = new DynamoDBClient({
customUserAgent: util.getUserAgentString(context.rootState.info.Version, 'C022'),
region: context.rootState.info.region, credentials
});
const response = await getParameters(context, dynamodb);
return response;
}
const failed = false;
module.exports = {
async startExport(context, opts) {
const info = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
const settings = await listSettings(context);
const merged = settings[2];
let headers;
if (merged.S3_PUT_REQUEST_ENCRYPTION && merged.S3_PUT_REQUEST_ENCRYPTION.length > 0) {
headers = { 'x-amz-server-side-encryption': merged.S3_PUT_REQUEST_ENCRYPTION };
console.log(`headers: ${headers}`);
}
await context.dispatch('_request', {
url: `${info._links.exports.href}/${opts.name}`,
method: 'put',
headers: headers || undefined,
body: opts.filter ? { filter: `${opts.filter}.*`, prefix: '' } : { prefix: '' },
});
},
async startKendraSyncExport(context, opts) {
console.log('Entering startKendraSyncExport function');
const info = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
await context.dispatch('_request', {
url: `${info._links.exports.href}/${opts.name}`,
method: 'put',
body: opts.filter ? { filter: `${opts.filter}.*`, prefix: 'kendra-' } : { prefix: 'kendra-' },
});
},
async downloadExport(context, opts) {
const credentials = context.rootState.user.credentials;
const s3 = new S3Client({
customUserAgent : util.getUserAgentString(context.rootState.info.Version, 'C011'),
region: context.rootState.info.region, credentials
});
const result = await s3.send(new GetObjectCommand({
Bucket: opts.bucket,
Key: opts.key,
}));
const qa = await result.Body.transformToString();
return `{"qna":[${qa.replace(/\n/g, ',\n')}]}`;
},
waitForExport(context, opts) {
return new Promise(async (res, rej) => {
await next(10);
async function next(count) {
try {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
const result = await context.dispatch('_request', {
url: response._links.imports.href,
method: 'get',
});
const job = result.jobs.find((x) => x.id === opts.id);
if (job) {
res(job);
} else {
count > 0 ? setTimeout(() => next(--count), 200) : rej('timeout');
}
} catch (error) {
rej(error);
}
}
});
},
async listExports(context, opts) {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
return context.dispatch('_request', {
url: response._links.exports.href,
method: 'get',
});
},
async getExport(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'get',
});
},
async getExportByJobId(context, id) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.jobs.href}/exports/${id}`,
method: 'get',
});
},
async deleteExport(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'delete',
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/genesys.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
getGenesysCallFlow(context, opts) {
return context.dispatch('_request', {
url: context.rootState.info._links.genesys.href,
method: 'get',
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/import.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const util = require('./../../../../capability/util');
module.exports = {
async listExamples(context) {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.examples.href,
method: 'get',
});
const examples = await Promise.all(response.examples.map(async (example) => {
if (_.get(example, 'description.href')) {
example.text = await context.dispatch('_request', {
url: example.description.href,
method: 'get',
});
}
return example;
}));
return examples;
},
async getExampleDescription(context, example) {
if (_.get(example, 'description.href')) {
return await context.dispatch('_request', {
url: example.description.href,
method: 'get',
});
}
},
async startImport(context, opts) {
const credentials = context.rootState.user.credentials;
const s3 = new S3Client({
customUserAgent : util.getUserAgentString(context.rootState.info.Version, 'C010'),
region: context.rootState.info.region, credentials
});
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
return s3.send(new PutObjectCommand({
Bucket: response._links.imports.bucket,
Key: response._links.imports.uploadPrefix + opts.name,
Body: opts.qa.map(JSON.stringify).join('\n'),
}));
},
waitForImport(context, opts) {
return new Promise(async (res, rej) => {
await next(10);
async function next(count) {
try {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
const result = await context.dispatch('_request', {
url: response._links.imports.href,
method: 'get',
});
const job = result.jobs.find((x) => x.id === opts.id);
if (job) {
res(job);
} else {
count > 0 ? setTimeout(() => next(--count), 200) : rej('timeout');
}
} catch (error) {
rej(error);
}
}
});
},
async listImports(context, opts) {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
return context.dispatch('_request', {
url: response._links.imports.href,
method: 'get',
});
},
getImport(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'get',
});
},
deleteImport(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'delete',
});
},
getTerminologies(context, opts) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.translate.href}/list`,
method: 'post',
});
},
startImportTranslate(context, opts) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.translate.href}/import`,
method: 'post',
body:
{
name: opts.name,
description: opts.description,
file: opts.file,
},
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const query = require('query-string').stringify;
const _ = require('lodash');
const axios = require('axios');
const { sign } = require('aws4');
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
let failed = false;
function handleTimeout(context, e) {
const login = _.get(context, 'rootState.info._links.DesignerLogin.href');
if (login && !failed) {
failed = true;
const result = window.confirm('Your credentials have expired. Click ok to be redirected to the login page.');
if (result) {
context.dispatch('user/logout', {}, { root: true });
window.window.location.href = login;
} else {
throw e;
}
}
}
function handleError(e, context, opts) {
const { status } = e.response;
if (status === 403) {
const login = _.get(context, 'rootState.info._links.DesignerLogin.href');
if (login && !failed) {
failed = true;
const result = window.confirm('You need to be logged in to use this page. click ok to be redirected to the login page');
if (result) window.window.location.href = login;
} else {
throw e;
}
} else {
const messageObj = {
response: _.get(e, 'response.data'),
status: _.get(e, 'response.status'),
};
if (status === 404 && opts.ignore404) {
throw new Error('does-not-exist');
} else if (messageObj?.response?.type === 'Error') {
throw new Error(messageObj?.response?.message)
} else {
window.alert('Request Failed: error response from endpoint');
throw messageObj;
}
}
}
module.exports = Object.assign(
require('./kendraIndex'),
require('./export'),
require('./import'),
require('./settings'),
require('./connect'),
require('./genesys'),
require('./testall'),
{
_request: async (context, opts) => {
const url = new URL(opts.url);
const request = {
host: url.hostname,
method: opts.method.toUpperCase(),
url: url.href,
path: url.pathname + url.search,
service: 'execute-api',
headers: opts.headers || {},
region: context.rootState.info.region,
};
if (opts.body) {
request.body = JSON.stringify(opts.body);
request.data = opts.body;
request.headers['content-type'] = 'application/json';
}
try {
const credentials = await mutex.runExclusive(async () => context.dispatch('user/getCredentials', {}, { root: true }));
const signed = sign(request, credentials);
delete request.headers.Host;
delete request.headers['Content-Length'];
context.commit('loading', true);
const result = await axios(signed);
return result.data;
} catch (e) {
console.log(JSON.stringify(_.get(e, 'response', e), null, 2));
if (e.response) {
handleError(e, context, opts);
} else if (e.name === 'CredentialTimeout') {
handleTimeout(context, e);
} else if (e.name === 'NotAuthorizedException') {
console.log('This user is not an authorized user.');
} else {
window.alert('Unknown Error');
throw e;
}
} finally {
context.commit('loading', false);
}
},
botinfo(context) {
return context.dispatch('_request', {
url: context.rootState.info._links.bot.href,
method: 'get',
reason: 'Failed to get BotInfo',
});
},
alexa(context) {
return context.dispatch('_request', {
url: context.rootState.bot._links.alexa.href,
method: 'get',
reason: 'Failed to get Alexa info',
});
},
schema(context, body) {
return context.dispatch('_request', {
url: context.rootState.info._links.questions.href,
method: 'options',
reason: 'Failed to get qa options',
});
},
list(context, opts) {
console.log(`Calling list with opts: ${JSON.stringify(opts)}`);
const perpage = opts.perpage || 100;
return context.dispatch('_request', {
url: `${context.rootState.info._links.questions.href}?${query({
from: (opts.page || 0) * perpage,
filter: opts.filter ? `${opts.filter}.*` : '',
order: opts.order,
perpage,
})}`,
method: 'get',
reason: `Failed to get page:${opts.page}`,
});
},
async check(context, qid) {
try {
await context.dispatch('_request', {
url: `${context.rootState.info._links.questions.href}/${encodeURIComponent(qid)}`,
method: 'head',
reason: `${qid} does not exists`,
ignore404: true,
});
return true;
} catch (x) {
if (x.message === 'does-not-exist') {
return false;
}
console.log(x);
throw x;
}
},
add(context, payload) {
return context.dispatch('update', payload);
},
update(context, payload) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.questions.href}/${encodeURIComponent(payload.qid)}`,
method: 'put',
body: payload,
reason: 'failed to update',
});
},
remove(context, qid) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.questions.href}/${encodeURIComponent(qid)}`,
method: 'delete',
reason: 'failed to delete',
});
},
removeBulk(context, list) {
return context.dispatch('_request', {
url: context.rootState.info._links.questions.href,
method: 'delete',
reason: 'failed to delete',
body: { list },
});
},
removeQuery(context, query) {
return context.dispatch('_request', {
url: context.rootState.info._links.questions.href,
method: 'delete',
reason: 'failed to delete',
body: { query },
});
},
build(context) {
return context.dispatch('_request', {
url: context.rootState.info._links.bot.href,
method: 'post',
body: {},
reason: 'failed to build',
});
},
status(context) {
return context.dispatch('_request', {
url: context.rootState.info._links.bot.href,
method: 'get',
reason: 'failed to get status',
});
},
search(context, opts) {
return context.dispatch('_request', {
url: `${context.rootState.info._links.questions.href}?${query({
query: opts.query,
topic: opts.topic || '',
client_filter: opts.client_filter || '',
score_answer: (opts.score_on === 'qna item answer') ? 'true' : 'false',
score_text_passage: (opts.score_on === 'text item passage') ? 'true' : 'false',
from: opts.from || 0,
})}`,
method: 'get',
reason: 'failed to get search',
});
},
},
);
================================================
FILE: source/website/js/lib/store/api/actions/kendraIndex.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
// point to new Kendra Lambda instead of the old one
startKendraV2Indexing(context, opts) {
return context.dispatch('_request', {
url: context.rootState.info._links.crawlerV2.href,
method: 'post',
});
},
getKendraIndexingStatus(context, opts) {
return context.dispatch('_request', {
url: context.rootState.info._links.crawlerV2.href,
method: 'get',
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/settings.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const { DynamoDBClient, DeleteItemCommand, GetItemCommand, PutItemCommand, ScanCommand, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const util = require('../../../../capability/util');
const defaultSettings = require('../../../../../../../source/lambda/cfn/lib/DefaultSettings.json');
const EMPTY_SENTINEL = 'EMPTY_STRING_BY_USER';
const chatbotTestingIndex = 0;
const languageSettingsIndex = 1;
const opensearchSettingsIndex = 2;
const securitySettingsIndex = 3;
const queryMatchingSettingsIndex = 4;
const settingsMap = {
baseChatbot: {
label: 'Base Chatbot Settings',
openedPanels: [
chatbotTestingIndex,
languageSettingsIndex,
opensearchSettingsIndex,
securitySettingsIndex,
queryMatchingSettingsIndex,
],
subgroups: {
chatbotTesting: {
id: 'chatbot_testing_subgroup',
label: 'ChatBot Testing',
collapsed: false,
members: [
{
id: 'ENABLE_DEBUG_RESPONSES',
type: 'boolean',
hint: 'Determines whether to log original English responses and translated responses for debugging',
},
{
id: 'ENABLE_DEBUG_LOGGING',
type: 'boolean',
hint: 'Controls verbosity of the QnABot Cloudwatch log. Set to true to see QnABot debug messages',
},
],
},
languageSettings: {
id: 'language_identification_subgroup',
label: 'Language Identification',
collapsed: false,
members: [
{
id: 'ENABLE_MULTI_LANGUAGE_SUPPORT',
type: 'boolean',
hint: 'Enable or Disable Amazon Translate support',
},
{
id: 'MINIMUM_CONFIDENCE_SCORE',
type: 'number',
hint: 'Enter number between 0.0 and 1.0. The minimum confidence before Amazon Comprehend will determine the user\'s language',
},
],
},
opensearchSettings: {
id: 'opensearch_subgroup',
label: 'OpenSearch',
collapsed: false,
members: [
{
id: 'ES_USE_KEYWORD_FILTERS',
type: 'boolean',
hint: 'Determines whether to detect keywords from Comprehend when searching for answers',
},
{
id: 'ES_SYNTAX_CONFIDENCE_LIMIT',
type: 'number',
hint: 'Enter number between 0.0 and 1.0. Amazon Comprehend makes a best effort to determine the parts of speech in a sentence. The keywords will only be used if the confidence limit is greater than this amount',
},
{
id: 'ES_MINIMUM_SHOULD_MATCH',
hint: 'See https://opensearch.org/docs/latest/query-dsl/minimum-should-match/ for syntax. Determines how close a question should match to return a hit',
},
{
id: 'ES_SCORE_ANSWER_FIELD',
type: 'boolean',
hint: 'Search the content of the answer field as a 2nd pass query (if there\'s no good match from 1st pass query on question fields)',
},
{
id: 'ES_SCORE_TEXT_ITEM_PASSAGES',
type: 'boolean',
hint: 'If no \'qna\' answer meets the score threshold, then query the text field of \'text\' items',
},
{
id: 'ERRORMESSAGE',
hint: 'Response to the user when a processing error occurs',
},
{
id: 'EMPTYMESSAGE',
hint: 'Response to the user when an answer could not be found',
},
],
},
securitySettings: {
id: 'security_and_privacy_subgroup',
label: 'Security and Privacy',
collapsed: false,
members: [
{
id: 'IDENTITY_PROVIDER_JWKS_URLS',
hint: 'Enter a comma-delimited list of URLs. Adds trusted IdPs (e.g. from Lex-Web-UI CognitoUserPoolPubKey)',
},
{
id: 'ENFORCE_VERIFIED_IDENTITY',
type: 'boolean',
hint: 'Set to true to make QnABot require verified identity from client',
},
{
id: 'NO_VERIFIED_IDENTITY_QUESTION',
hint: 'If user identity cannot be verified, replace question string with this',
},
{
id: 'ENABLE_REDACTING',
type: 'boolean',
hint: 'Enables or disables the system\'s ability to redact log output using REDACTING_REGEX',
},
{
id: 'REDACTING_REGEX',
hint: 'Defines patterns to be redacted from logs when ENABLE_REDACTING is true',
},
{
id: 'ENABLE_REDACTING_WITH_COMPREHEND',
type: 'boolean',
hint: 'Enables PII Redaction using Amazon Comprehend. See: https://aws.amazon.com/blogs/machine-learning/detecting-and-redacting-pii-using-amazon-comprehend/',
},
{
id: 'COMPREHEND_REDACTING_CONFIDENCE_SCORE',
type: 'number',
hint: 'Enter a number between 0.0 and 1.0 to set a threshold for PII redaction. Only PII detected with Amazon Comprehend\'s confidence score higher than this value will be redacted.',
},
{
id: 'COMPREHEND_REDACTING_ENTITY_TYPES',
hint: 'Enter a comma-separated list of values. A list of PII Entity Types. See: https://aws.amazon.com/blogs/machine-learning/detecting-and-redacting-pii-using-amazon-comprehend/',
},
{
id: 'PII_REJECTION_ENABLED',
type: 'boolean',
hint: 'Enables or disables the system\'s ability to reject input containing PII. It is recommended to also enable PII redaction by setting the ENABLE_REDACTING and/or the ENABLE_REDACTING_WITH_COMPREHEND if you are enabling PII rejection.',
},
{
id: 'PII_REJECTION_QUESTION',
hint: 'If PII rejection is enabled and PII is detected, the user\'s original question will be replaced with this text.',
},
{
id: 'PII_REJECTION_REGEX',
hint: 'Defines patterns to identify PII for rejection purposes.',
},
{
id: 'PII_REJECTION_ENTITY_TYPES',
hint: 'Enter a comma separated list of PII Entity Categories (https://aws.amazon.com/blogs/machine-learning/detecting-and-redacting-pii-using-amazon-comprehend/). Only recognize PII entity types in the list',
},
{
id: 'PII_REJECTION_CONFIDENCE_SCORE',
type: 'number',
hint: 'Enter a number between 0.0 and 1.0 to set a threshold for PII rejection. Only PII detected with Amazon Comprehend\'s confidence score higher than this value will trigger rejection',
},
{
id: 'DISABLE_CLOUDWATCH_LOGGING',
type: 'boolean',
hint: 'Disable all logging in fulfillment es query handler lambda. does not disable logging from Lambda Hooks or Conditional Chaining Lambda functions',
},
{
id: 'MINIMAL_ES_LOGGING',
type: 'boolean',
hint: 'Set to true to not log utterances or session attributes to OpenSearch for OpenSearchDashboards logging',
},
{
id: 'S3_PUT_REQUEST_ENCRYPTION',
hint: 'Enable header x-amz-server-side-encryption header and set with this value',
},
],
},
queryMatchingSettings: {
id: 'query_matching_subgroup',
label: 'Query Matching',
collapsed: false,
members: [
{
id: 'SEARCH_REPLACE_QUESTION_SUBSTRINGS',
hint: 'replace words or phrases in user questions by defining search/replace pairs in a JSON object like: {"searchString":"replaceString"}. Add additional pairs separated by commas, like: {"searchString":"replaceString", "searchString2":"replaceString2"}',
},
{
id: 'PROTECTED_UTTERANCES',
hint: 'A comma-separated list of utterances that will not be translated or disambiguated by QnABot. Each phrase is not case sensitive and ignores common punctuation characters: .,!;-?',
},
{
id: 'EMBEDDINGS_ENABLE',
type: 'boolean',
hint: 'Disable use of semantic search using embeddings. Set to TRUE only if QnABot stack was deployed with embeddings enabled',
},
{
id: 'EMBEDDINGS_SCORE_THRESHOLD',
type: 'number',
hint: 'Enter a number between 0.0 and 1.0. If embedding similarity score is under threshold the match is rejected and QnABot reverts to scoring answer field (if ES_SCORE_ANSWER_FIELD is true)',
},
{
id: 'EMBEDDINGS_SCORE_ANSWER_THRESHOLD',
type: 'number',
hint: 'Enter a number between 0.0 and 1.0. Applies only when if ES_SCORE_ANSWER_FIELD is true. If embedding similarity score on answer field is under threshold the match is rejected',
},
{
id: 'EMBEDDINGS_TEXT_PASSAGE_SCORE_THRESHOLD',
type: 'number',
hint: 'Enter a number between 0.0 and 1.0. Applies only when if ES_SCORE_TEXT_ITEM_PASSAGES is true. If embedding similarity score on text item field is under threshold the match is rejected',
},
{
id: 'LLM_GENERATE_QUERY_ENABLE',
type: 'boolean',
hint: 'Enables query disambiguation feature which generates a query when disambiguating follow-up questions',
},
{
id: 'LLM_GENERATE_QUERY_PROMPT_TEMPLATE',
type: 'textarea',
hint: 'Customize the prompt template to send to the LLM to generate a query when disambiguating follow-up questions',
},
{
id: 'LLM_GENERATE_QUERY_MODEL_PARAMS',
hint: 'Customize the inference parameters sent to the LLM model specified for LLMApi when disambiguating follow-up questions (e.g. When selecting LLMApi as BEDROCK and LLMBedrockModelId as an Anthropic model, the inference parameters can be customized as `{"temperature":0.1}` or `{"temperature":0.3, "maxTokens": 262, "topP":0.9, "top_k": 240 }`). To find more details on supported parameters for your model provider, please check the LLM model documentation for values that your specific model accepts.',
},
{
id: 'LLM_GENERATE_QUERY_SYSTEM_PROMPT',
hint: 'Customize the system prompt for LLMBedrockModelId when disambiguating user\'s question based on the chat history. A system prompt is a type of prompt that provides instructions or context to the model about the task it should perform, or the persona it should adopt during the conversation. For more information on models that support system prompt, please refer to (https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html)',
}
],
},
advancedSettings: {
id: 'advanced_subgroup',
label: 'Advanced',
collapsed: true,
members: [
{
id: 'USER_HISTORY_TTL_DAYS',
type: 'number',
hint: 'The number of days to keep user and chat history in DynamoDB before expiring. If you would like your user/chat history to never expire, leave this value as 0.',
},
{
id: 'ES_EXPAND_CONTRACTIONS',
hint: 'Expand contractions to resolve problems with keyword filters',
},
{
id: 'ES_KEYWORD_SYNTAX_TYPES',
hint: 'Enter comma-separated values. See https://docs.aws.amazon.com/comprehend/latest/dg/how-syntax.html. A list of tokens representing parts of speech identified by Amazon Comprehend for matching questions',
},
{
id: 'ES_NO_HITS_QUESTION',
hint: 'The question QnABot should use when it cannot find an answer',
},
{
id: 'ES_ERROR_QUESTION',
hint: 'The question QnABot should use when a backend error ocurred',
},
{
id: 'ES_USE_FUZZY_MATCH',
type: 'boolean',
hint: 'Determines whether QnABot should return answers similar to the question asked. See https://opensearch.org/docs/latest/query-dsl/term/fuzzy/ for more information',
},
{
id: 'ES_PHRASE_BOOST',
type: 'number',
hint: 'If the user\'s question is a phrase match to a question in the knowledge then boost the score by this factor',
},
{
id: 'ENABLE_SENTIMENT_SUPPORT',
type: 'boolean',
hint: 'Enables Amazon Comprehend be used for sentiment analysis. See: https://docs.aws.amazon.com/comprehend/latest/dg/how-sentiment.html',
},
{
id: 'ENABLE_CUSTOM_TERMINOLOGY',
type: 'boolean',
hint: 'Enable support for installed Custom Terminology files when using Amazon Translate. See: https://aws.amazon.com/blogs/machine-learning/introducing-amazon-translate-custom-terminology/',
},
{
id: 'ALT_SEARCH_KENDRA_FAQ_CONFIDENCE_SCORE',
type: 'enum',
enums: ['VERY_HIGH', 'HIGH', 'MEDIUM', 'LOW'],
hint: 'Minimum Kendra confidence level threshold for Kendra FAQ. See: https://aws.amazon.com/about-aws/whats-new/2020/09/amazon-kendra-launches-confidence-scores/',
},
{
id: 'ALT_SEARCH_KENDRA_FAQ_MESSAGE',
hint: 'Heading when a Frequently Asked Question is found by Amazon Kendra.- See: https://docs.aws.amazon.com/kendra/latest/dg/response-types.html',
},
{
id: 'KENDRA_FAQ_CONFIG_MAX_RETRIES',
type: 'number',
hint: 'Number of times to retry syncing FAQ\'s when a throttling error occurs',
},
{
id: 'KENDRA_FAQ_CONFIG_RETRY_DELAY',
type: 'number',
hint: 'Amount of time to wait in seconds between attempts to retry syncing',
},
{
id: 'KENDRA_FAQ_ES_FALLBACK',
type: 'boolean',
hint: 'When Kendra FAQ is enabled, but does not return an answer then query OpenSearch',
},
{
id: 'ENABLE_KENDRA_WEB_INDEXER',
type: 'boolean',
hint: 'Enables the web indexer',
},
{
id: 'KENDRA_INDEXER_URLS',
hint: 'Enter comma-separated values. List of web addresses QnABot should crawl and index with Kendra',
},
{
id: 'KENDRA_INDEXER_CRAWL_DEPTH',
type: 'number',
hint: 'Sets the depth to the number of levels in a website from the seed level that you want to crawl',
},
{
id: 'KENDRA_INDEXER_CRAWL_MODE',
type: 'enum',
enums: ['HOST_ONLY', 'SUBDOMAINS', 'EVERYTHING'],
hint: 'Determines which addresses should be crawled',
},
{
id: 'KENDRA_INDEXER_SCHEDULE',
hint: 'See https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html for CloudWatch Rate Syntax. Interval Indexer should crawl',
},
{
id: 'KENDRA_INDEXED_DOCUMENTS_LANGUAGES',
hint: 'Enter comma-separated values. Should be one of supported Kendra languages mentioned here: https://docs.aws.amazon.com/kendra/latest/dg/in-adding-languages.html',
},
{
id: 'SMS_HINT_REMINDER_ENABLE',
type: 'boolean',
hint: 'Enables SMS_HINT_REMINDER',
},
{
id: 'SMS_HINT_REMINDER',
hint: 'Reminds user how to use the bot on first use after SMS_HINT_REMINDER_INTERVAL_HRS',
},
{
id: 'SMS_HINT_REMINDER_INTERVAL_HRS',
type: 'number',
hint: 'The amount of time in hours when to send SMS_HINT_REMINDER',
},
{
id: 'RUN_LAMBDAHOOK_FROM_QUERY_STEP',
type: 'boolean',
hint: 'Controls timing of execution for Lambda hooks',
},
{
id: 'LAMBDA_PREPROCESS_HOOK',
hint: 'name of AWS Lambda to run before each question is processed. The name of the Lambda must start with "qna-" or "QNA-" to comply with the permissions of the role attached to the Fulfillment Lambda',
},
{
id: 'LAMBDA_POSTPROCESS_HOOK',
hint: 'name of AWS Lambda to run after the question is processed. But before user profile information is saved. The name of the Lambda must start with "qna-" or "QNA-" to comply with the permissions of the role attached to the Fulfillment Lambda',
},
{
id: 'EMBEDDINGS_MAX_TOKEN_LIMIT',
hint: 'Max number of tokens the embeddings model can handle',
},
{
id: 'LLM_PROMPT_MAX_TOKEN_LIMIT',
hint: 'Specifies the maximum number of tokens in the prompt message that can be sent to the LLM. QnABot will selectively truncate the prompt by history and context to shorten the total length',
},
],
},
},
},
addins: {
label: 'Add Ins and Connections Settings',
openedPanels: [],
subgroups: {
lexSettings: {
id: 'amazon_lex_subgroup',
label: 'Amazon Lex',
collapsed: true,
members: [
{
id: 'ELICIT_RESPONSE_MAX_RETRIES',
type: 'number',
hint: 'Number of times an elicitResponse LexBot can fail before QnaBot gives up and does not ask the user to start the elicitResponse LexBot workflow again',
},
{
id: 'ELICIT_RESPONSE_RETRY_MESSAGE',
hint: 'Retry message displayed by QnABot when the elicitResponse LexBot workflow fails and the user has to start again',
},
{
id: 'ELICIT_RESPONSE_BOT_FAILURE_MESSAGE',
hint: 'Failure message displayed by QnaBot when the maximum number of retries of the elicitResponse LexBot workflow is exceeded',
},
{
id: 'ELICIT_RESPONSE_DEFAULT_MSG',
hint: 'Default closing response message used by QnAbot when the elicitResponse LexBot does not return a closing response to QnABot',
},
{
id: 'BOT_ROUTER_WELCOME_BACK_MSG',
hint: 'The text used by QnABot when ending communication from a specialty bot',
},
{
id: 'BOT_ROUTER_EXIT_MSGS',
hint: 'Enter comma-separated values. The exit phrases in comma separated list available for the a user to end communication with a specialty bot',
},
],
},
connectSettings: {
id: 'amazon_connect_subgroup',
label: 'Amazon Connect',
collapsed: true,
members: [
{
id: 'CONNECT_IGNORE_WORDS',
hint: 'Throw an error if the transcript provided by connect __only__ contains the words in this list (case insensitive). This is useful if you find many missed utterances due to the use of filler words before a proper utterance (e.g. "a", "e", "umm", "like", etc.). This setting can not be used as a transcript filter (see `LAMBDA_PREPROCESS_HOOK` or `LAMBDA_POSTPROCESS_HOOK` if you wish to apply custom processing to questions/answers)',
},
{
id: 'CONNECT_ENABLE_VOICE_RESPONSE_INTERRUPT',
type: 'boolean',
hint: 'Return bot response in session attribute to enable contact flow to use response as an interruptible prompt',
},
{
id: 'CONNECT_NEXT_PROMPT_VARNAME',
hint: 'Name of session var to use for next prompt',
},
],
},
alexaSettings: {
id: 'amazon_alexa_subgroup',
label: 'Amazon Alexa',
collapsed: true,
members: [
{
id: 'DEFAULT_ALEXA_LAUNCH_MESSAGE',
hint: 'Initial greeting when using Alexa',
},
{
id: 'DEFAULT_ALEXA_REPROMPT',
hint: 'Default text used for Alexa reprompt capability',
},
{
id: 'DEFAULT_ALEXA_STOP_MESSAGE',
hint: 'User response to end session with Alexa',
},
],
},
},
},
rag: {
label: 'Text Generation using LLMs',
openedPanels: [],
subgroups: {
generalSettings: {
id: 'text_generation_general_subgroup',
label: 'General Settings',
collapsed: true,
members: [
{
id: 'LLM_QA_ENABLE',
type: 'boolean',
hint: 'Enables or disables generative answers from passages retrieved via embeddings or Kendra fallback when no FAQ match is found. Applied only to passages and Kendra results - does not apply when an FAQ/QID matches the question',
},
{
id: 'LLM_QA_PROMPT_TEMPLATE',
type: 'textarea',
hint: 'Customize the prompt template used to construct a prompt for LLM to generate an answer from the context of retrieved passages (applicable for Text Item passages or RAG with Kendra)',
},
{
id: 'LLM_QA_MODEL_PARAMS',
hint: 'Customize the inference parameters sent to the LLM model specified for LLMApi when generating answers to questions (e.g. When selecting LLMApi as BEDROCK and LLMBedrockModelId as an Anthropic model, the inference parameters can be customized as `{"temperature":0.1}` or `{"temperature":0.3, "maxTokens": 262, "topP":0.9, "top_k": 240 }`). To find more details on supported parameters for your model provider, please check the LLM model documentation for values that your specific model accepts.',
},
{
id: 'LLM_QA_PREFIX_MESSAGE',
hint: 'Message used to prefix LLM-generated answer',
},
{
id: 'LLM_QA_SHOW_CONTEXT_TEXT',
type: 'boolean',
hint: 'Enables or disables inclusion of the passages used as context for LLM-generated answers',
},
{
id: 'LLM_QA_SHOW_SOURCE_LINKS',
type: 'boolean',
hint: 'Enables or disables Kendra Source Links or passage refMarkdown links (document references) in markdown answers',
},
{
id: 'LLM_CHAT_HISTORY_MAX_MESSAGES',
type: 'number',
hint: 'Specifies the maximum number of previous messages maintained in the QnABot DynamoDB UserTable for conversational context and follow-up question disambiguation',
},
{
id: 'LLM_QA_SYSTEM_PROMPT',
hint: 'Customize the system prompt for LLMBedrockModelId when generating an answer to the user\'s question. A system prompt is a type of prompt that provides instructions or context to the model about the task it should perform, or the persona it should adopt during the conversation. For more information on models that support system prompt, please refer to (https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html)',
},
{
id: 'LLM_QA_NO_HITS_REGEX',
hint: 'Enter a regular expression. If the LLM response matches the specified pattern (e.g., "Sorry, I don\'t know"), the response is treated as no_hits, and the default EMPTYMESSAGE or a custom \'no_hits\' item is returned instead. Disabled by default, since enabling it prevents easy debugging of LLM don\'t know responses',
},
{
id: 'FALLBACK_ORDER',
type: 'enum',
enums: ['KNOWLEDGEBASE-FIRST', 'KENDRA-FIRST'],
hint: 'Specifies the order in which the fallback mechanisms (Amazon Kendra and Amazon Bedrock Knowledge Base) should be tried. By default, the QnABot will try RAG with Amazon Bedrock Knowledge Base first, and if no hits are returned, it will then try RAG with Amazon Kendra. This setting only takes effect when both BedrockKnowledgeBaseId and AltSearchKendraIndexes are provided in the CloudFormation deployment.',
},
],
},
bedrockGuardrails: {
id: 'text_generation_guardrail_subgroup',
label: 'Amazon Bedrock Guardrails Integration',
collapsed: true,
members: [
{
id: 'BEDROCK_GUARDRAIL_IDENTIFIER',
hint: 'Enter a pre-configurated Amazon Bedrock Guardrail identifier (e.g. 4ojm24q0yada) that you want to be applied to the requests made to the LLM models configured in CloudFormation parameters LLMBedrockModelId and BedrockKnowledgeBaseModel. If you don\'t provide a value, no guardrail is applied to the LLM invocation. If you provide a identifier, you must also provide a BEDROCK_GUARDRAIL_VERSION',
},
{
id: 'BEDROCK_GUARDRAIL_VERSION',
hint: 'Enter the version (e.g. 1 or DRAFT) of the guardrail specifed in BEDROCK_GUARDRAIL_IDENTIFIER',
},
{
id: 'PREPROCESS_GUARDRAIL_IDENTIFIER',
hint: 'Enter a pre-configurated Amazon Bedrock Guardrail identifier (e.g. 4ojm24q0yada) that you want to be applied to the input query to block harmful content or detected PII entities before processing (PREPROCESS) user\'s utterance in the fulfillment. If you don\'t provide a value, no guardrail is applied in the preprocessing step. If you provide a identifier, you must also provide a PREPROCESS_GUARDRAIL_VERSION',
},
{
id: 'PREPROCESS_GUARDRAIL_VERSION',
hint: 'Enter the version (e.g. 1 or DRAFT) of the guardrail specifed in PREPROCESS_GUARDRAIL_IDENTIFIER',
},
{
id: 'POSTPROCESS_GUARDRAIL_IDENTIFIER',
hint: 'Enter a pre-configurated Amazon Bedrock Guardrail identifier (e.g. 4ojm24q0yada) that you want to be applied to the final answer after processing of the user\'s utterance has completed in the postprocessing step of fulfillment. If you don\'t provide a value, no guardrail is applied in the postprocessing step. If you provide a identifier, you must also provide a POSTPROCESS_GUARDRAIL_VERSION',
},
{
id: 'POSTPROCESS_GUARDRAIL_VERSION',
hint: 'Enter the version (e.g. 1 or DRAFT) of the guardrail specifed in POSTPROCESS_GUARDRAIL_IDENTIFIER',
},
],
},
kendraSettings: {
id: 'amazon_kendra_subgroup',
label: 'Retrieval Augmented Generation (RAG) with Amazon Kendra',
collapsed: true,
members: [
{
id: 'LLM_QA_USE_KENDRA_RETRIEVAL_API',
type: 'boolean',
hint: 'Enables or disables use of Kendra\'s new retrieval API. When enabled, QnABot uses Kendra Retrieve api to retrieve semantically relevant passages of up to 200 token words from the documents (not FAQs) in your index. When disabled, QnAbot use Kendra Query to retrieve shorter passages or answers. Takes effect only when LLM_QA_ENABLE is true. The default is true (recommended) when LLM QA is enabled. See https://docs.aws.amazon.com/kendra/latest/APIReference/API_Retrieve.html',
},
{
id: 'ALT_SEARCH_KENDRA_FALLBACK_CONFIDENCE_SCORE',
type: 'enum',
enums: ['VERY_HIGH', 'HIGH', 'MEDIUM', 'LOW'],
hint: 'Answers will only be returned that or at or above the specified confidence level (https://aws.amazon.com/about-aws/whats-new/2020/09/amazon-kendra-launches-confidence-scores/) when using Kendra Fallback. This setting does not affect the filtering of results for Kendra retrieval used when an LLM is enabled',
},
{
id: 'ALT_SEARCH_KENDRA_S3_SIGNED_URLS',
type: 'boolean',
hint: 'Enables signed S3 (https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) Urls for Amazon Kendra results. If enabled, allows support for Kendra documents which are not publicly accessible. Please ensure IAM FulfillmentLambdaRole has access to S3 objects in Kendra index (default role grants access to buckets starting with name QNA or qna)',
},
{
id: 'ALT_SEARCH_KENDRA_S3_SIGNED_URL_EXPIRE_SECS',
type: 'number',
hint: 'Determines length of time in seconds for the validity of signed S3 Urls in Kendra fallback',
},
{
id: 'ALT_SEARCH_KENDRA_MAX_DOCUMENT_COUNT',
type: 'number',
hint: 'Number of documents returned by Amazon Kendra fallback',
},
{
id: 'ALT_SEARCH_KENDRA_TOP_ANSWER_MESSAGE',
hint: 'Heading when the top answer (https://docs.aws.amazon.com/kendra/latest/dg/response-types.html) is found by Amazon Kendra',
},
{
id: 'ALT_SEARCH_KENDRA_ANSWER_MESSAGE',
hint: 'Heading when a Document (https://docs.aws.amazon.com/kendra/latest/dg/response-types.html) is returned by Amazon Kendra',
},
{
id: 'ALT_SEARCH_KENDRA_RESPONSE_TYPES',
hint: 'Enter comma-separated values of valid Amazon Kendra response types (https://docs.aws.amazon.com/kendra/latest/dg/response-types.html). Kendra fallback will only return responses of the listed types',
},
{
id: 'ALT_SEARCH_KENDRA_ABBREVIATE_MESSAGE_FOR_SSML',
type: 'boolean',
hint: 'If a set to "true", an abbreviate Amazon Kendra response will be sent via voice. If set to "false", the full text of the Kendra fallback response will be sent when using voice',
},
],
},
bedrockSettings: {
id: 'amazon_bedrock_knowledge_bases_subgroup',
label: 'Retrieval Augmented Generation (RAG) with Amazon Bedrock Knowledge Base',
collapsed: true,
members: [
{
id: 'KNOWLEDGE_BASE_PROMPT_TEMPLATE',
type: 'textarea',
hint: 'The template used to construct a prompt that is sent to the model for response generation. To opt out of sending a prompt to the Knowledge Base model, simply leave this field empty. For more information, see Bedrock Knowledge base (https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html)',
},
{
id: 'KNOWLEDGE_BASE_PREFIX_MESSAGE',
hint: 'Message used to prefix a Knowledge Base generated answer',
},
{
id: 'KNOWLEDGE_BASE_SHOW_REFERENCES',
type: 'boolean',
hint: 'Enables or disables inclusion of the passages used as context for Bedrock Knowledge Base generated answers',
},
{
id: 'KNOWLEDGE_BASE_S3_SIGNED_URLS',
type: 'boolean',
hint: 'Enables or disables S3 presigned URL signing for Bedrock Knowledge Base answers',
},
{
id: 'KNOWLEDGE_BASE_S3_SIGNED_URL_EXPIRE_SECS',
type: 'number',
hint: 'Determines length of time in seconds for the validity of signed S3 Urls for Bedrock Knowledge Base answers',
},
{
id: 'KNOWLEDGE_BASE_MODEL_PARAMS',
hint: 'Customize the inference parameters sent to the LLM model specified in cloudformation parameter BedrockKnowledgeBaseModel (e.g. anthropic model parameters can be customized as `{"temperature":0.1}` or `{"temperature":0.3, "maxTokens": 262, "topP":0.9, "top_k": 240 }`). For more information, please refer to Inference parameters (https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html)',
},
{
id: 'KNOWLEDGE_BASE_MAX_NUMBER_OF_RETRIEVED_RESULTS',
type: 'number',
hint: 'Sets maximum number of retrieved result where each result corresponds to a source chunk. When querying a knowledge base, Amazon Bedrock returns up to five results by default. For more information, please refer to Maximum number of retrieved results (https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html)',
},
{
id: 'KNOWLEDGE_BASE_SEARCH_TYPE',
type: 'enum',
enums: ['DEFAULT', 'HYBRID', 'SEMANTIC'],
hint: 'Select the search type which defines how data sources in the knowledge base are queried. If using an Amazon OpenSearch Serverless vector store that contains a filterable text field, you can specify whether to query the knowledge base with a HYBRID search using both vector embeddings and raw text, or SEMANTIC search using only vector embeddings. For other vector store configurations, only SEMANTIC search is available. For more information, please refer to Search type (https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html)',
},
{
id: 'KNOWLEDGE_BASE_METADATA_FILTERS',
hint: 'Specifies the filters to use on the metadata in the knowledge base data sources before returning results. (e.g filters can be customized as`{"filter1": { "key": "string", "value": "string" }, "filter2": { "key": "string", "value": number }}`). For more information, please refer to Metadata and filtering (https://docs.aws.amazon.com/bedrock/latest/userguide/kb-test-config.html)',
},
],
},
},
},
};
async function getParameters(context, dynamodb) {
const tableName = context.rootState.info.SettingsTable;
const params = {
TableName: tableName,
FilterExpression: "SettingCategory <> :private",
ExpressionAttributeValues: {
":private": {
"S": "Private"
}
}
};
const custom_settings = {};
const default_settings = {}
let lastEvaluatedKey = null;
do {
if (lastEvaluatedKey) {
params.ExclusiveStartKey = lastEvaluatedKey;
}
try {
const command = new ScanCommand(params);
const response = await dynamodb.send(command);
response.Items.forEach(item => {
const unmarshalledItem = unmarshall(item);
const settingName = unmarshalledItem.SettingName;
const settingValue = unmarshalledItem.SettingValue;
const defaultValue = unmarshalledItem.DefaultValue;
const settingCategory = unmarshalledItem.SettingCategory;
if (settingValue === EMPTY_SENTINEL) {
custom_settings[settingName] = "";
} else if (settingValue !== "") {
custom_settings[settingName] = settingValue;
}
if (settingCategory == "Custom") {
custom_settings[settingName] = settingValue;
}
default_settings[settingName] = defaultValue;
});
lastEvaluatedKey = response.LastEvaluatedKey;
} catch (error) {
console.error('Error scanning DynamoDB table:', error);
throw error;
}
} while (lastEvaluatedKey);
let cloned_default = _.clone(default_settings)
let merged_settings = _.merge(cloned_default, custom_settings)
return [default_settings, custom_settings, merged_settings];
}
async function saveParameters(context, dynamodb, settings) {
const tableName = context.rootState.info.SettingsTable;
let changedSettings = []
Object.entries(settings).forEach(async ([settingName, settingValue]) => {
let settingCategory;
if (defaultSettings[settingName]) {
settingCategory = defaultSettings[settingName]["SettingCategory"]
} else
settingCategory = "Custom"
console.log(`Setting Name ${settingName}, Setting Value ${settingValue}`)
const getParams = {
TableName: tableName,
Key: marshall({ SettingName: settingName }),
};
const getCommand = new GetItemCommand(getParams);
const currentSetting = await dynamodb.send(getCommand);
if(!currentSetting.Item && settingCategory == "Custom") {
const item = {
SettingName: settingName,
SettingValue: settingValue,
SettingCategory: "Custom",
nonce: 0
};
const putParams = {
TableName: tableName,
Item: marshall(item)
};
const putCommand = new PutItemCommand(putParams);
const result = await dynamodb.send(putCommand);
changedSettings.push(settingName)
return result;
} else if (!currentSetting.Item) {
throw new Error(`Setting ${settingName} not found`);
}
const unmarshalledItem = unmarshall(currentSetting.Item);
const currentNonce = unmarshalledItem.nonce;
const updateParams = {
TableName: context.rootState.info.SettingsTable,
Key: marshall({ SettingName: settingName }),
UpdateExpression: "SET #value = :value, #nonce = :newNonce",
ConditionExpression: "#nonce = :currentNonce",
ExpressionAttributeNames: {
"#value": "SettingValue",
"#nonce": "nonce"
},
ExpressionAttributeValues: marshall({
":value": settingValue,
":currentNonce": currentNonce,
":newNonce": currentNonce + 1 // Increment the nonce
}),
ReturnValues: "UPDATED_NEW"
};
const updateCommand = new UpdateItemCommand(updateParams);
let result;
try {
result = await dynamodb.send(updateCommand);
} catch(error) {
console.error(`Failed to update setting ${settingName}`);
throw error;
}
changedSettings.push(settingName)
console.log(`Successfully updated ${settingName}: ${JSON.stringify(result)}`);
});
return changedSettings;
}
async function checkForRestoredSettings(context, dynamodb, settings){
const tableName = context.rootState.info.SettingsTable;
const modified_settings = await getParameters(context,dynamodb);
const restoredSettings = _.omit(modified_settings[1], Object.keys(settings));
let restoredSettingsList = [];
Object.keys(restoredSettings).forEach(async (settingName) => {
let settingCategory;
if (defaultSettings[settingName]) {
settingCategory = defaultSettings[settingName]["SettingCategory"]
} else
settingCategory = "Custom"
if (settingCategory == "Custom") {
const params = {
TableName: tableName,
Key: marshall({ SettingName: settingName })
};
try {
const updateCommand = new DeleteItemCommand(params);
await dynamodb.send(updateCommand);
}
catch {
throw new Error(`Failed to delete custom setting ${settingName}`);
}
restoredSettingsList.push(settingName)
} else {
const getParams = {
TableName: tableName,
Key: marshall({ SettingName: settingName }),
};
const getCommand = new GetItemCommand(getParams);
const currentSetting = await dynamodb.send(getCommand);
if (!currentSetting.Item) {
throw new Error(`Setting ${settingName} not found`);
}
const unmarshalledItem = unmarshall(currentSetting.Item);
const currentNonce = unmarshalledItem.nonce;
const params = {
TableName: tableName,
Key: marshall({ SettingName: settingName }),
UpdateExpression: 'SET SettingValue = :emptyValue, #nonce = :newNonce',
ConditionExpression: "#nonce = :currentNonce",
ExpressionAttributeNames: {
"#nonce": "nonce"
},
ExpressionAttributeValues: marshall({
':emptyValue': "", // Set to empty string
":currentNonce": currentNonce,
":newNonce": currentNonce + 1 // Increment the nonce
}),
ReturnValues: 'NONE'
};
const updateCommand = new UpdateItemCommand(params);
let result;
try {
result = await dynamodb.send(updateCommand);
} catch {
throw new Error(`Failed to restore setting ${settingName}`);
}
restoredSettingsList.push(settingName)
console.log(`Successfully restored ${settingName}: ${JSON.stringify(result)}`);
}
});
return restoredSettingsList;
}
function createSettingsMap(settings) { // NOSONAR - javascript:S3776 - settings map needs to be anonymized
return {
event: 'UPDATE_SETTINGS',
BEDROCK_GUARDRAIL_ENABLE: settings.BEDROCK_GUARDRAIL_IDENTIFIER && settings.BEDROCK_GUARDRAIL_VERSION ? 'true' : 'false',
PREPROCESS_GUARDRAIL_ENABLE: settings.PREPROCESS_GUARDRAIL_IDENTIFIER && settings.PREPROCESS_GUARDRAIL_VERSION ? 'true' : 'false',
POSTPROCESS_GUARDRAIL_ENABLE: settings.POSTPROCESS_GUARDRAIL_IDENTIFIER && settings.POSTPROCESS_GUARDRAIL_VERSION ? 'true' : 'false',
ENABLE_MULTI_LANGUAGE_SUPPORT: settings.ENABLE_MULTI_LANGUAGE_SUPPORT || 'false',
LLM_GENERATE_QUERY_ENABLE: settings.LLM_GENERATE_QUERY_ENABLE || 'true',
KNOWLEDGE_BASE_SEARCH_TYPE: settings.KNOWLEDGE_BASE_SEARCH_TYPE || 'DEFAULT',
KNOWLEDGE_BASE_METADATA_FILTERS_ENABLE: settings.KNOWLEDGE_BASE_METADATA_FILTERS && settings.KNOWLEDGE_BASE_METADATA_FILTERS !== "{}" ? 'true' : 'false',
PII_REJECTION_ENABLED: settings.PII_REJECTION_ENABLED || 'false',
EMBEDDINGS_ENABLE: settings.EMBEDDINGS_ENABLE || 'true',
LLM_QA_ENABLE: settings.LLM_QA_ENABLE || 'true',
FALLBACK_ORDER: settings.FALLBACK_ORDER || 'KNOWLEDGEBASE-FIRST',
ENABLE_REDACTING: settings.ENABLE_REDACTING || 'false',
ENABLE_REDACTING_WITH_COMPREHEND: settings.ENABLE_REDACTING_WITH_COMPREHEND || 'false'
};
}
async function sendAnonymizedData(params, settings){
const map = createSettingsMap(settings);
const payload = Buffer.from(JSON.stringify(map));
const client = new LambdaClient({
customUserAgent: util.getUserAgentString(params.version, 'C050'),
region: params.region,
credentials: params.credentials
});
const command = new InvokeCommand({
FunctionName: params.solutionHelper,
InvocationType: "Event",
Payload: payload,
});
const response = await client.send(command);
if (response.FunctionError) {
throw new Error('Solution Helper Function Error Occurred');
}
return response;
}
module.exports = {
async listSettings(context) {
const credentials = context.rootState.user.credentials;
const dynamodb = new DynamoDBClient({
customUserAgent: util.getUserAgentString(context.rootState.info.Version, 'C022'),
region: context.rootState.info.region, credentials
});
const response = await getParameters(context, dynamodb);
return response;
},
async listPrivateSettings(context) {
const { credentials } = context.rootState.user;
const dynamodb = new DynamoDBClient({
customUserAgent: util.getUserAgentString(context.rootState.info.Version, 'C022'),
region: context.rootState.info.region, credentials
});
try {
const params = {
TableName: context.rootState.info.SettingsTable,
FilterExpression: "SettingCategory = :private",
ExpressionAttributeValues: {
":private": {
"S": "Private"
}
}
}
const scanCommand = new ScanCommand(params)
const response = await dynamodb.send(scanCommand);
let privateSettings = {}
response.Items.forEach(item => {
const unmarshalledItem = unmarshall(item);
const settingName = unmarshalledItem.SettingName;
const settingValue = unmarshalledItem.SettingValue;
privateSettings[settingName] = settingValue;
});
return privateSettings;
} catch (error) {
console.error(`Error while fetching custom parameters ${error}`);
return {};
}
},
async updateSettings(context, settings) {
const credentials = context.rootState.user.credentials;
const region = context.rootState.info.region;
const version = context.rootState.info.Version;
const solutionHelper = context.rootState.info.SolutionHelper;
const dynamodb = new DynamoDBClient({
customUserAgent: util.getUserAgentString(version, 'C022'),
region,
credentials
});
try {
const params = {
region,
credentials,
version,
solutionHelper
};
await sendAnonymizedData(params, settings);
} catch (e) {
console.log(`Error in sending anonymized data: ${e.message}`);
}
try {
const changedSettings = await saveParameters(context, dynamodb, settings);
const restoredSettings = await checkForRestoredSettings(context, dynamodb, settings);
return {changedSettings, restoredSettings};
} catch (error) {
throw new Error('Failed to update or restore settings: '+ error);
}
},
getSettingsMap() {
return settingsMap;
},
};
================================================
FILE: source/website/js/lib/store/api/actions/testall.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const util = require('../../../../capability/util');
module.exports = {
async startTestAll(context, opts) {
const info = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
const body = opts.filter ? { filter: `${opts.filter}.*` } : {};
body.token = `${opts.token}`;
body.locale = `${opts.locale}` != '' ? `${opts.locale}` : 'en_US';
await context.dispatch('_request', {
url: `${info._links.testall.href}/${opts.name}`,
method: 'put',
body,
});
},
async downloadTestAll(context, opts) {
const credentials = context.rootState.user.credentials;
const s3 = new S3Client({
customUserAgent : util.getUserAgentString(context.rootState.info.Version, 'C012'),
region: context.rootState.info.region, credentials
});
const result = await s3.send(new GetObjectCommand({
Bucket: opts.bucket,
Key: opts.key,
}));
const download = await result.Body.transformToString()
return download;
},
waitForTestAll(context, opts) {
return new Promise(async (res, rej) => {
await next(10);
async function next(count) {
try {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
const result = await context.dispatch('_request', {
url: response._links.testall.href,
method: 'get',
});
const job = result.jobs.find((x) => x.id === opts.id);
if (job) {
res(job);
} else {
count > 0 ? setTimeout(() => next(--count), 200) : rej('timeout');
}
} catch (error) {
rej(error);
}
}
});
},
async listTestAll(context, opts) {
const response = await context.dispatch('_request', {
url: context.rootState.info._links.jobs.href,
method: 'get',
});
return context.dispatch('_request', {
url: response._links.testall.href,
method: 'get',
});
},
getTestAll(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'get',
});
},
deleteTestAll(context, opts) {
return context.dispatch('_request', {
url: opts.href,
method: 'delete',
});
},
getBotInfo(context) {
return context.dispatch('_request', {
url: context.rootState.info._links.bot.href,
method: 'get',
reason: 'Failed to get BotInfo',
});
},
};
================================================
FILE: source/website/js/lib/store/api/actions/tmp.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const region = 'us-east-1';
const s3 = new S3Client({ region });
run();
async function run() {
const result = await s3.send(new GetObjectCommand({
Bucket: 'qna-dev-dev-master-40-exportbucket-fgiztk0ghtl5',
Key: 'data/qna.jsond',
}));
const raw = await result.Body.transformToString();
console.log(`[${raw.replace(/$/g, ',')}]`);
}
================================================
FILE: source/website/js/lib/store/api/card-schema.json
================================================
{
"type":"object",
"properties":{
"attachmentLinkUrl":{
"type":"string"
},
"buttons":{
"type":"array",
"items":{
"type":"object",
"properties":{
"text":{"type":"string"},
"value":{"type":"string"}
},
"require":["text","value"],
"additionalProperties":false
}
},
"imageUrl":{
"type":"string"
},
"subTitle":{
"type":"string"
},
"title":{
"type":"string"
}
},
"additionalProperties":false
}
================================================
FILE: source/website/js/lib/store/api/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vuex = require('vuex');
module.exports = {
namespaced: true,
state: {
loading: false,
},
mutations: {
loading(state, val) {
state.loading = val;
},
},
getters: {},
actions: require('./actions'),
};
================================================
FILE: source/website/js/lib/store/api/schema.json
================================================
{
"type":"object",
"properties":{
"qna":{
"type":"array",
"items":{
"type":"object",
"properties":{
"q":{
"type":"array",
"items":{
"type":"string"
}
},
"a":{
"type":"string"
},
"qid":{
"type":"string"
},
"r":{
"type":["object"]
},
"t":{
"type":"string"
}
},
"required":["q","a","qid"]
}
}
},
"required":["qna"]
}
================================================
FILE: source/website/js/lib/store/data/actions/add.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('./util');
const { api } = util;
async function next(count, res, rej, context, result) {
try {
const info = await api(context, 'botinfo');
if (info.build.token === result.token) {
context.rootState.bot.status = info.build.status;
context.rootState.bot.build.message = info.build.message;
if (info.build.status === 'READY') {
res();
} else if (info.build.status === 'Failed') {
rej(`build failed:${info.build.message}`);
} else {
count > 0 ? setTimeout(async () => await next(--count, res, rej, context, result), 1000)
: rej(' build timed out');
}
} else {
context.rootState.bot.status = 'Waiting';
count > 0 ? setTimeout(async () => await next(--count, res, rej, context, result), 1000)
: rej(' build timed out');
}
} catch (e) {
rej(e);
}
}
module.exports = {
async build(context) {
context.rootState.bot.status = 'Submitting';
context.rootState.bot.build.message = '';
context.rootState.bot.build.token = '';
context.rootState.bot.build.status = '';
try {
let result = await api(context, 'botinfo');
if (result.status === 'READY') {
result = await api(context, 'build');
context.rootState.bot.build.token = result.token;
} else if (result.status === 'BUILDING') {
return;
} else {
return Promise.reject(`cannot build, bot in state ${result.status}`);
}
await new Promise((res) => setTimeout(res, 200));
context.rootState.bot.build.token = result.token;
await new Promise((res, rej) => {
next(60 * 5, res, rej, context, result);
});
} catch (e) {
util.handle.bind(context)('Failed to Build')(e);
throw e;
}
},
async update(context, qa) {
return await api(context, 'update', clean(_.omit(qa, ['select', '_score'])));
},
async add(context, qa) {
await api(context, 'update', clean(qa));
context.commit('page/incrementTotal', null, { root: true });
},
};
function clean(obj) {
if (typeof obj === 'object') {
for (const key in obj) {
obj[key] = clean(obj[key]);
}
return obj;
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = clean(obj[i]);
}
} else if (obj.trim) {
return obj.trim();
} else {
return obj;
}
}
================================================
FILE: source/website/js/lib/store/data/actions/delete.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('./util');
const { api } = util;
module.exports = {
async removeQ(context, { index, item }) {
try {
item.questions.splice(index, 1);
await context.dispatch('update', { qa: item });
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to remove');
}
},
async removeQA(context, QA) {
const index = context.state.QAs.findIndex((qa) => qa.qid === QA.qid);
if (index >= 0) {
try {
await api(context, 'remove', QA.qid);
context.commit('delQA', QA);
context.commit('page/decrementTotal', null, { root: true });
} catch (e) {
console.log('Error:', e);
}
}
return Promise.resolve();
},
async removeQAs(context, QAs) {
try {
const qids = QAs.map((x) => x.qid);
if (qids.length > 0) {
await api(context, 'removeBulk', qids);
}
context.state.QAs = context.state.QAs.filter((x) => !qids.includes(x.qid));
context.commit('page/decrementTotal', qids.length, { root: true });
} catch (e) {
console.log('Error:', e);
}
},
async removeFilter(context) {
try {
const filter = context.state.filter ? `${context.state.filter}.*` : '.*';
await api(context, 'removeQuery', filter);
await new Promise((res) => setTimeout(res, 2000));
context.commit('clearQA');
context.commit('clearFilter');
await context.dispatch('get', {});
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to remove');
}
},
};
================================================
FILE: source/website/js/lib/store/data/actions/get.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const util = require('./util');
const { api } = util;
module.exports = {
async schema(context) {
const x = await api(context, 'schema');
context.commit('schema', x);
},
async botinfo(context) {
try {
const data = await api(context, 'botinfo');
context.commit('bot', data, { root: true });
const alexa = await api(context, 'alexa');
context.commit('alexa', alexa, { root: true });
} catch (e) {
console.log('Error:', e);
throw new Error('Failed get BotInfo');
}
},
async search(context, opts) {
try {
_.defaults(opts, {
query: opts.query,
topic: opts.topic,
perpage: opts.perpage,
});
const result = await api(context, 'search', opts);
context.commit('clearQA');
context.state.QAs = result.qa.map((x) => util.parse(x, context));
context.commit('page/setTotal', result.total, { root: true });
return result.qa.length;
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to search');
}
},
async get(context, opts = {}) {
try {
context.commit('loading', "primary");
_.defaults(opts, {
filter: context.state.filter || '.*',
order: opts.order || 'asc',
perpage: opts.perpage,
});
const result = await api(context, 'list', opts);
context.commit('clearQA');
context.state.QAs = result.qa.map((x) => util.parse(x, context));
context.commit('page/setTotal', result.total, { root: true });
return result.qa.length;
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to get');
} finally {
context.commit('loading', false);
}
},
async getAll(context) {
context.commit('clearQA');
return new Promise((resolve, reject) => {
const next = (index) => context.dispatch('get', { page: index })
.then((count) => (count < 1 ? resolve() : next(++index)))
.catch((err) => reject(err));
next(0);
})
.catch((e) => {
console.log('Error:', e);
throw new Error('Failed to getAll');
});
},
};
================================================
FILE: source/website/js/lib/store/data/actions/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const validator = new (require('jsonschema').Validator)();
const axios = require('axios');
const util = require('./util');
module.exports = Object.assign(
require('./get'),
require('./delete'),
require('./up-download'),
require('./add'),
);
================================================
FILE: source/website/js/lib/store/data/actions/schema.json
================================================
{
"type":"object",
"properties":{
"qna":{
"type":"array",
"items":{
"type":"object",
"properties":{
"q":{
"type":"array",
"items":{
"type":"string"
}
},
"a":{
"type":"string"
},
"sa":{
"type": "array",
"items": {
"type":["object"]
}
},
"qid":{
"type":"string"
},
"r":{
"type":["object"]
}
},
"required":["q","a","qid"]
}
}
},
"required":["qna"]
}
================================================
FILE: source/website/js/lib/store/data/actions/up-download.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const validator = new (require('jsonschema').Validator)();
const axios = require('axios');
const util = require('./util');
const { api } = util;
module.exports = {
async download(context) {
try {
const result = await api(context, 'list', { from: 'all' });
console.log(result);
const blob = new Blob(
[JSON.stringify({ qna: result.qa }, null, 3)],
{ type: 'text/plain;charset=utf-8' },
);
return blob;
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to download');
}
},
downloadLocal(context) {
const qna = context.state.QAs.map((qa) => ({
q: qa.questions.map((item) => item.text),
a: qa.answer.text,
r: JSON.parse(qa.card.text),
qid: qa.qid.text,
}));
const blob = new Blob(
[JSON.stringify({ qna }, null, 3)],
{ type: 'text/plain;charset=utf-8' },
);
return Promise.resolve(blob);
},
async downloadSelect(context) {
try {
const filter = context.state.selectIds.map((literal_string) => literal_string.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&')).join('|');
const result = await api(context, 'list', { from: 'all', filter: `(${filter})` });
console.log(result);
const blob = new Blob(
[JSON.stringify({ qna: result.qa }, null, 3)],
{ type: 'text/plain;charset=utf-8' },
);
return blob;
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to download the select');
}
},
async upload(context, params) {
try {
let out;
if (params.data) {
out = await context.dispatch('uploadProcess', { data: params.data });
} else if (params.url) {
out = await context.dispatch('uploadUrl', { url: params.url });
} else {
return Promise.reject('invalid params');
}
return out;
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to upload');
}
},
async uploadProcess(context, { data }) {
try {
const v = validator.validate(data, require('./schema.json'));
await (async () => {
if (v.valid) {
return api(context, 'bulk', data);
}
console.log(v);
return Promise.reject(`Invalid QnA:${v.errors.map((err) => err.stack).join(',')}`);
})();
context.commit('clearQA');
await new Promise((res) => setTimeout(res, 2000));
return context.dispatch('get', 0);
} catch (e) {
console.log('Error:', e);
throw new Error('Failed in upload process');
}
},
async uploadUrl(context, { url }) {
try {
const response = await Promise.resolve(axios.get(url));
const { data } = response;
return context.dispatch('upload', { data });
} catch (e) {
console.log('Error:', e);
throw new Error('Error: please check URL and source CORS configuration');
}
},
};
================================================
FILE: source/website/js/lib/store/data/actions/util.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const validator = new (require('jsonschema').Validator)();
const axios = require('axios');
const _ = require('lodash');
exports.api = function (context, name, args) {
return context.dispatch(`api/${name}`, args, { root: true });
};
exports.parse = function (item, context) {
_.defaults(item, {
_score: 0,
q: [],
t: '',
r: {
title: '',
text: '',
url: '',
},
select: false,
});
return item;
};
exports.handle = function (reason) {
const self = this;
return function (err) {
console.log('Error:', err);
self.commit('setError', reason, { root: true });
return Promise.reject(reason);
};
};
exports.load = async function (list) {
const self = this;
try {
const results = await Promise.resolve(list);
if (!results.qa) {
throw new Error('Failed to access qa in the list');
}
results.qa.forEach((result) => {
self.commit('addQA', exports.parse(result, self));
self.commit('page/setTotal', self.state.QAs.length, { root: true });
});
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to load');
}
};
================================================
FILE: source/website/js/lib/store/data/getters.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
selected(state) {
return state.QAs.map((qa) => qa.select);
},
QAlist(state, getters, rootGetters) {
if (rootGetters.page.mode !== 'test') {
return state.QAs.sort((a, b) => {
if (a.qid.text < b.qid.text) return -1;
if (a.qid.text > b.qid.text) return 1;
return 0;
});
}
return state.QAs.sort((a, b) => b.score - a.score);
},
};
================================================
FILE: source/website/js/lib/store/data/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vuex = require('vuex');
module.exports = {
namespaced: true,
state: {
QAs: [],
schema: {},
filter: '',
loading: false,
},
mutations: require('./mutations'),
getters: require('./getters'),
actions: require('./actions'),
};
================================================
FILE: source/website/js/lib/store/data/mutations.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
close(store) {
const check = (el) => el.text === el.tmp;
const any = store.QAs.map((qa) => qa.questions.map(check).concat([
check(qa.answer),
check(qa.qid),
check(qa.card.imageUrl),
check(qa.card.title),
]).includes(false)).includes(true);
if (any) {
store.commit('setError', 'Please save or cancel your work', { root: true });
return false;
}
store.QAs.forEach((qa) => {
qa.open = false;
qa.edit = false;
});
return true;
},
selectAll(store, value) {
store.QAs.map((x) => x.select = value);
},
setFilter(store, query) {
store.filter = query;
},
clearFilter(store) {
store.filter = null;
},
addQA(state, qa) {
qa.selected = false;
state.QAs.unshift(qa);
},
schema(state, schema) {
state.schema = schema;
},
delQA(state, QA) {
const index = state.QAs.findIndex((qa) => qa.qid === QA.qid);
state.QAs.splice(index, 1);
},
clearQA(state) {
state.QAs = [];
},
results(state, new_results) {
state.results = new_results;
},
loading(state, val) {
state.loading = val;
}
};
================================================
FILE: source/website/js/lib/store/getters.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = { };
================================================
FILE: source/website/js/lib/store/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vuex = require('vuex');
const { createStore } = require('vuex');
module.exports = createStore({
state: {
info: {},
bot: {
status: '',
message: '',
utterances: [],
alexa: {},
connect: {},
genesys: {},
},
alexa: {},
connect: {},
genesys: {},
error: '',
},
mutations: require('./mutations'),
getters: require('./getters'),
actions: require('./actions'),
modules: {
user: require('./user'),
api: require('./api'),
data: require('./data'),
page: require('./page'),
},
});
================================================
FILE: source/website/js/lib/store/mutations.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
captureHash(state) {
state.hash = location.hash.substring(1);
},
info(state, payload) {
state.info = payload;
},
bot(state, payload) {
const tmp = state.bot.utterances;
state.bot = payload;
state.bot.utterances = tmp;
},
utterances(state, payload) {
state.bot.utterances = payload;
},
alexa(state, payload) {
state.bot.alexa = payload;
},
setBotInfo(store, data) {
data.lambdaName = data.lambdaArn.match(/arn:aws:lambda:.*:.*:function:(.*)/)[1]; // NOSONAR - javascript:S5852 - input is user controlled and we have a limit on the number of characters
store.bot = data;
},
setError(store, message) {
store.error = message;
},
clearError(store) {
store.error = null;
},
};
================================================
FILE: source/website/js/lib/store/page/actions.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const validator = new (require('jsonschema').Validator)();
const axios = require('axios');
const util = require('./util');
const { api } = util;
module.exports = {
setMode(context, mode) {
context.commit('setMode', mode);
if (mode === 'questions') {
context.dispatch('goToPage', context.state.current);
}
},
async goToPage(context, index) {
context.commit('data/clearQA', null, { root: true });
context.commit('setPage', index);
try {
await context.dispatch('data/get', index, { root: true });
} catch (error) {
console.log('Error:', error);
throw new Error(('Failed to Build'));
}
},
nextPage(context) {
let index = context.state.current + 1;
const total = Math.ceil(context.state.total / context.state.perpage);
index = index > total - 1 ? total - 1 : index;
return context.dispatch('goToPage', index);
},
previousPage(context) {
let index = context.state.current - 1;
index = index < 0 ? 0 : index;
return context.dispatch('goToPage', index);
},
};
================================================
FILE: source/website/js/lib/store/page/getters.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
pages(state) {
return Math.ceil(state.total / state.perpage);
},
};
================================================
FILE: source/website/js/lib/store/page/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vuex = require('vuex');
module.exports = {
namespaced: true,
state: {
loaded: 0,
mode: 'questions',
current: 0,
perpage: 15,
total: 0,
},
mutations: require('./mutations'),
getters: require('./getters'),
actions: require('./actions'),
};
================================================
FILE: source/website/js/lib/store/page/mutations.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
setMode(store, mode) {
store.mode = mode;
},
setPage(store, page) {
store.current = page;
},
setTotal(store, total) {
store.total = total;
},
incrementTotal(store, count) {
const x = count || 1;
store.page += x;
},
decrementTotal(store, count) {
const x = count || 1;
store.page -= x;
},
toggleMode(store, mode) {
for (const x in store.mode) {
if (x === mode) {
if (mode === 'filter') {
store.mode[x].on = !store.mode[x].on;
} else {
store.mode[x] = !store.mode[x];
}
} else if (x === 'filter') {
store.mode[x].on = false;
} else {
store.mode[x] = false;
}
}
},
toggleSearch(store) {
store.mode.search = !store.mode.search;
},
toggleFilter(store) {
store.mode.filter = !store.mode.filter;
},
};
================================================
FILE: source/website/js/lib/store/page/util.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const validator = new (require('jsonschema').Validator)();
const axios = require('axios');
exports.api = function (context, name, args) {
return context.dispatch(`api/${name}`, args, { root: true });
};
exports.parse = function (item, context) {
if (!item.body.r) {
item.body.r = {
title: '',
imageUrl: '',
};
}
return {
qid: {
text: item.body.qid,
tmp: item.body.qid,
},
answer: {
text: item.body.a,
tmp: item.body.a,
},
card: {
text: JSON.stringify(item.body.r, null, 4) || '',
title: {
text: item.body.r.title || '',
tmp: item.body.r.title || '',
},
imageUrl: {
text: item.body.r.imageUrl || '',
tmp: item.body.r.imageUrl || '',
},
},
topic: {
text: item.body.t || '',
tmp: item.body.t || '',
},
questions: item.body.q.map((Q) => ({ text: Q, tmp: Q })),
open: false,
edit: false,
select: context.state.selectIds.includes(item.body.qid),
deleting: false,
score: item.score || 0,
};
};
exports.handle = function (reason) {
const self = this;
return function (err) {
console.log('Error:', err);
self.commit('setError', reason, { root: true });
return Promise.reject(reason);
};
};
exports.load = async function (list) {
const self = this;
self.commit('startLoading');
try {
const results = await Promise.resolve(list);
if (!results.qa) {
throw new Error('Failed to access qa in the list');
}
results.qa.forEach((result) => self.commit('addQA', parse(result, self)));
self.commit('setTotal', self.state.QAs.length);
} catch (e) {
console.log('Error:', e);
throw new Error('Failed to load');
} finally {
self.commit('stopLoading');
}
};
================================================
FILE: source/website/js/lib/store/user/actions.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const axios = require('axios');
const jwt = require('jsonwebtoken');
const _ = require('lodash');
require('vue');
const query = require('query-string');
const { fromCognitoIdentityPool } = require('@aws-sdk/credential-providers');
const { CognitoIdentityProviderClient, AdminUserGlobalSignOutCommand } = require('@aws-sdk/client-cognito-identity-provider');
const util = require('../../../capability/util');
const provideCredentials = async (context) => {
const region = context.rootState.info.region;
const logins = {};
logins[[
'cognito-idp.',
context.rootState.info.region,
'.amazonaws.com/',
context.rootState.info.UserPool,
].join('')] = context.state.token;
const credentialProvider = fromCognitoIdentityPool({
identityPoolId: context.rootState.info.PoolId,
logins,
clientConfig: { region },
})
const credentials = await credentialProvider();
context.state.credentials = credentials;
return context.state.credentials;
};
const getTokens = async (context, code) => {
const endpoint = context.rootState.info._links.CognitoEndpoint.href;
const clientId = context.rootState.info.ClientIdDesigner;
try {
const tokens = await axios({
method: 'POST',
url: `${endpoint}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'authorization_code',
client_id: clientId,
code,
redirect_uri: window.location.origin + window.location.pathname,
}),
});
window.sessionStorage.setItem('id_token', tokens.data.id_token);
window.sessionStorage.setItem('access_token', tokens.data.access_token);
window.sessionStorage.setItem('refresh_token', tokens.data.refresh_token);
context.state.token = tokens.data.id_token;
return tokens.data.id_token;
} catch (e) {
const loginUrl = _.get(context, 'rootState.info._links.DesignerLogin.href');
const result = window.confirm('Unable to fetch credentials, please log back in. Click Ok to be redirected to the login page.');
if (result) {
await context.dispatch('logout');
window.location.href = loginUrl;
}
}
};
const login = async (context) => {
const id_token = window.sessionStorage.getItem('id_token');
let token;
if (id_token && id_token !== 'undefined') {
token = jwt.decode(id_token);
context.state.token = id_token;
} else {
const { code } = query.parse(window.location.search);
token = jwt.decode(await getTokens(context, code));
}
context.state.name = token['cognito:username'];
context.state.groups = token['cognito:groups'];
if (!context.state.groups || !context.state.groups.includes('Admins')) {
const loginUrl = _.get(context.rootState, 'info._links.DesignerLogin.href');
window.alert('You must be an administrative user to view this page');
window.location.href = loginUrl;
}
};
const logout = async (context) => {
const redirectUrl = window.location.origin + window.location.pathname;
const cognitoEndpoint = context.rootState.info._links.CognitoEndpoint.href;
const username = context.rootState.user.name;
const clientId = context.rootState.info.ClientIdDesigner;
const userpool = context.rootState.info.UserPool;
const region = context.rootState.info.region;
try {
const credentials = await provideCredentials(context);
const client = new CognitoIdentityProviderClient({
region,
credentials,
customUserAgent: util.getUserAgentString(context.rootState.info.Version, 'C023'),
});
const adminSignOutCmd = new AdminUserGlobalSignOutCommand({
UserPoolId: userpool,
Username: username,
});
const signOutResponse = await client.send(adminSignOutCmd);
console.log('Admin Global Sign Out Status Code: ', signOutResponse?.$metadata.httpStatusCode);
} catch (e) {
console.log(`Error fetching credentials ${e.message.substring(0, 500)}`);
}
// clear context state credential
if (context?.state?.credentials) {
delete context.state.credentials;
}
if (context?.rootState?.user?.credentials) {
delete context.rootState.user.credentials;
}
// clear session and local storage
window.sessionStorage.clear();
window.localStorage.clear();
// redirect to logout url
const logoutUrl = `${cognitoEndpoint}/logout?response_type=code&client_id=${clientId}&redirect_uri=${redirectUrl}`;
window.location.replace(logoutUrl);
};
const getCredentials = async (context) => {
let credentials;
try {
if (!_.get(context, 'state.credentials')) {
credentials = await provideCredentials(context);
return credentials;
}
if (context.state.credentials.expiration && new Date(context.state.credentials.expiration) <= new Date()) {
credentials = await provideCredentials(context);
return credentials;
}
return context.state.credentials;
} catch (e) {
console.log(`Error getting credentials ${e.message.substring(0, 500)}`);
if (e.message.match('Token expired') || e.message.match('inactive')) {
await context.dispatch('refreshTokens');
return await provideCredentials(context);
}
throw e;
}
};
const refreshTokens = async (context) => {
console.log('refreshing tokens');
const refresh_token = window.sessionStorage.getItem('refresh_token');
const endpoint = context.rootState.info._links.CognitoEndpoint.href;
const clientId = context.rootState.info.ClientIdDesigner;
try {
const tokens = await axios({
method: 'POST',
url: `${endpoint}/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: query.stringify({
grant_type: 'refresh_token',
client_id: clientId,
refresh_token,
}),
});
window.sessionStorage.setItem('id_token', tokens.data.id_token);
window.sessionStorage.setItem('access_token', tokens.data.access_token);
window.sessionStorage.setItem('refresh_token', tokens.data.refresh_token);
context.state.token = tokens.data.id_token;
} catch (e) {
const loginUrl = _.get(context, 'rootState.info._links.DesignerLogin.href');
const result = window.confirm('Your credentials have expired, please log back in. Click Ok to be redirected to the login page.');
if (result) {
await context.dispatch('logout');
window.location.href = loginUrl;
}
}
};
module.exports = { refreshTokens, getCredentials, logout, login };
================================================
FILE: source/website/js/lib/store/user/getters.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = {
};
================================================
FILE: source/website/js/lib/store/user/index.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const Vuex = require('vuex');
module.exports = {
namespaced: true,
state: {
loggedin: false,
},
mutations: require('./mutations'),
getters: require('./getters'),
actions: require('./actions'),
};
================================================
FILE: source/website/js/lib/store/user/mutations.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
const _ = require('lodash');
const query = require('query-string');
const jwt = require('jsonwebtoken');
module.exports = {
credentials(state, payload) {
state.loggedin = true;
state.credentials = payload;
},
login(state) {
state.loggedIn = true;
},
setId(state, Id) {
state.Id = Id;
},
};
================================================
FILE: source/website/js/lib/store/user/schema.json
================================================
{
"type":"object",
"properties":{
"qna":{
"type":"array",
"items":{
"type":"object",
"properties":{
"q":{
"type":"array",
"items":{
"type":"string"
}
},
"a":{
"type":"string"
},
"qid":{
"type":"string"
},
"r":{
"type":["object"]
}
},
"required":["q","a","qid"]
}
}
},
"required":["qna"]
}
================================================
FILE: source/website/js/lib/validator.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
module.exports = function (App) {
App.$validator.extend('json', {
getMessage: (field) => 'invalid json',
validate(value) {
try {
const card = JSON.parse(value);
const v = new (require('jsonschema').Validator)();
const { valid } = v.validate(card, require('./store/api/card-schema.json'));
return valid;
} catch (e) {
return false;
}
},
});
App.$validator.extend('optional', {
getMessage: (field) => 'invalid characters',
validate(value) {
try {
return !!value.match(/.*/);
} catch (e) {
return false;
}
},
});
};
================================================
FILE: source/website/js/test.js
================================================
/** ************************************************************************************************
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. *
* SPDX-License-Identifier: Apache-2.0 *
************************************************************************************************ */
window.horde = gremlins.createHorde();
window.horde.gremlin(gremlins.species.clicker())
.gremlin(gremlins.species.formFiller())
.gremlin(gremlins.species.typer());
window.start = function (logout = true) {
logout = document.getElementById('logout');
if (logout) logout.hidden = logout;
window.horde.unleash();
};
================================================
FILE: source/website/style/app.styl
================================================
$theme := {
primary: #1fbcd3
accent: #ffbb00
secondary: #3157d5
info: #0D47A1
warning: #ffba21
error: #a71000
success: #1ddf48
}
@require '../../node_modules/vuetify/dist/vuetify.min.css'
================================================
FILE: source/website/styles/app.css
================================================
a:not([class]){
color : #1fbcd3 !important;
}
.v-btn--variant-elevated {
background-color: #f5f5f5;
}
================================================
FILE: source/website/styles/fonts/material-icons.css
================================================
.material-icons {
font-family: 'Material Icons'; /*NOSONAR - Required for material icons*/
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-webkit-font-feature-settings: 'liga';
text-rendering: optimizeLegibility;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
}
================================================
FILE: source/website/styles/pure-min.css
================================================
/*!
Pure v1.0.0
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/yahoo/pure/blob/master/LICENSE.md
*/
/*!
normalize.css v^3.0 | MIT License | git.io/normalize
Copyright (c) Nicolas Gallagher and Jonathan Neal
*/
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
.pure-button:focus,
a:active,
a:hover {
outline: 0
}
.pure-table,
table {
border-collapse: collapse;
border-spacing: 0
}
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block
}
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline
}
audio:not([controls]) {
display: none;
height: 0
}
[hidden],
template {
display: none
}
a {
background-color: transparent
}
abbr[title] {
border-bottom: 1px dotted
}
b,
optgroup,
strong {
font-weight: 700
}
dfn {
font-style: italic
}
h1 {
font-size: 2em;
margin: .67em 0
}
mark {
background: #ff0;
color: #000
}
small {
font-size: 80%
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sup {
top: -.5em
}
sub {
bottom: -.25em
}
img {
border: 0
}
svg:not(:root) {
overflow: hidden
}
figure {
margin: 1em 40px
}
hr {
box-sizing: content-box;
height: 0
}
pre,
textarea {
overflow: auto
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0
}
.pure-button,
input {
line-height: normal
}
button {
overflow: visible
}
button,
select {
text-transform: none
}
button,
html input[type=button],
input[type=reset],
input[type=submit] {
-webkit-appearance: button;
cursor: pointer
}
button[disabled],
html input[disabled] {
cursor: default
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0
}
input[type=checkbox],
input[type=radio] {
box-sizing: border-box;
padding: 0
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
height: auto
}
input[type=search] {
-webkit-appearance: textfield;
box-sizing: content-box
}
.pure-button,
.pure-form input:not([type]),
.pure-menu {
box-sizing: border-box
}
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration {
-webkit-appearance: none
}
fieldset {
border: 1px solid silver;
margin: 0 2px;
padding: .35em .625em .75em
}
legend,
td,
th {
padding: 0
}
legend {
border: 0
}
.hidden,
[hidden] {
display: none !important
}
.pure-img {
max-width: 100%;
height: auto;
display: block
}
.pure-g {
letter-spacing: -.31em;
text-rendering: optimizespeed;
font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-flow: row wrap;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start
}
@media all and (-ms-high-contrast:none),
(-ms-high-contrast:active) {
table .pure-g {
display: block
}
}
.opera-only :-o-prefocus,
.pure-g {
word-spacing: -.43em
}
.pure-u,
.pure-u-1,
.pure-u-1-1,
.pure-u-1-12,
.pure-u-1-2,
.pure-u-1-24,
.pure-u-1-3,
.pure-u-1-4,
.pure-u-1-5,
.pure-u-1-6,
.pure-u-1-8,
.pure-u-10-24,
.pure-u-11-12,
.pure-u-11-24,
.pure-u-12-24,
.pure-u-13-24,
.pure-u-14-24,
.pure-u-15-24,
.pure-u-16-24,
.pure-u-17-24,
.pure-u-18-24,
.pure-u-19-24,
.pure-u-2-24,
.pure-u-2-3,
.pure-u-2-5,
.pure-u-20-24,
.pure-u-21-24,
.pure-u-22-24,
.pure-u-23-24,
.pure-u-24-24,
.pure-u-3-24,
.pure-u-3-4,
.pure-u-3-5,
.pure-u-3-8,
.pure-u-4-24,
.pure-u-4-5,
.pure-u-5-12,
.pure-u-5-24,
.pure-u-5-5,
.pure-u-5-6,
.pure-u-5-8,
.pure-u-6-24,
.pure-u-7-12,
.pure-u-7-24,
.pure-u-7-8,
.pure-u-8-24,
.pure-u-9-24 {
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
display: inline-block;
zoom: 1
}
.pure-g [class*=pure-u] {
font-family: sans-serif
}
.pure-u-1-24 {
width: 4.1667%
}
.pure-u-1-12,
.pure-u-2-24 {
width: 8.3333%
}
.pure-u-1-8,
.pure-u-3-24 {
width: 12.5%
}
.pure-u-1-6,
.pure-u-4-24 {
width: 16.6667%
}
.pure-u-1-5 {
width: 20%
}
.pure-u-5-24 {
width: 20.8333%
}
.pure-u-1-4,
.pure-u-6-24 {
width: 25%
}
.pure-u-7-24 {
width: 29.1667%
}
.pure-u-1-3,
.pure-u-8-24 {
width: 33.3333%
}
.pure-u-3-8,
.pure-u-9-24 {
width: 37.5%
}
.pure-u-2-5 {
width: 40%
}
.pure-u-10-24,
.pure-u-5-12 {
width: 41.6667%
}
.pure-u-11-24 {
width: 45.8333%
}
.pure-u-1-2,
.pure-u-12-24 {
width: 50%
}
.pure-u-13-24 {
width: 54.1667%
}
.pure-u-14-24,
.pure-u-7-12 {
width: 58.3333%
}
.pure-u-3-5 {
width: 60%
}
.pure-u-15-24,
.pure-u-5-8 {
width: 62.5%
}
.pure-u-16-24,
.pure-u-2-3 {
width: 66.6667%
}
.pure-u-17-24 {
width: 70.8333%
}
.pure-u-18-24,
.pure-u-3-4 {
width: 75%
}
.pure-u-19-24 {
width: 79.1667%
}
.pure-u-4-5 {
width: 80%
}
.pure-u-20-24,
.pure-u-5-6 {
width: 83.3333%
}
.pure-u-21-24,
.pure-u-7-8 {
width: 87.5%
}
.pure-u-11-12,
.pure-u-22-24 {
width: 91.6667%
}
.pure-u-23-24 {
width: 95.8333%
}
.pure-u-1,
.pure-u-1-1,
.pure-u-24-24,
.pure-u-5-5 {
width: 100%
}
.pure-button {
display: inline-block;
zoom: 1;
white-space: nowrap;
vertical-align: middle;
text-align: center;
cursor: pointer;
-webkit-user-drag: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.pure-button::-moz-focus-inner {
padding: 0;
border: 0
}
.pure-button-group {
letter-spacing: -.31em;
text-rendering: optimizespeed
}
.opera-only :-o-prefocus,
.pure-button-group {
word-spacing: -.43em
}
.pure-button {
font-family: inherit;
font-size: 100%;
padding: .5em 1em;
color: #444;
color: rgba(0, 0, 0, .8);
border: 1px solid #999;
border: transparent;
background-color: #E6E6E6;
text-decoration: none;
border-radius: 2px
}
.pure-button-hover,
.pure-button:focus,
.pure-button:hover {
filter: alpha(opacity=90);
background-image: -webkit-linear-gradient(transparent, rgba(0, 0, 0, .05) 40%, rgba(0, 0, 0, .1));
background-image: linear-gradient(transparent, rgba(0, 0, 0, .05) 40%, rgba(0, 0, 0, .1))
}
.pure-button-active,
.pure-button:active {
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15) inset, 0 0 6px rgba(0, 0, 0, .2) inset;
border-color: #000\9
}
.pure-button-disabled,
.pure-button-disabled:active,
.pure-button-disabled:focus,
.pure-button-disabled:hover,
.pure-button[disabled] {
border: none;
background-image: none;
filter: alpha(opacity=40);
opacity: .4;
cursor: not-allowed;
box-shadow: none;
pointer-events: none
}
.pure-button-hidden {
display: none
}
.pure-button-primary,
.pure-button-selected,
a.pure-button-primary,
a.pure-button-selected {
background-color: #0078e7;
color: #fff
}
.pure-button-group .pure-button {
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
margin: 0;
border-radius: 0;
border-right: 1px solid #111;
border-right: 1px solid rgba(0, 0, 0, .2)
}
.pure-button-group .pure-button:first-child {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px
}
.pure-button-group .pure-button:last-child {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-right: none
}
.pure-form input[type=password],
.pure-form input[type=email],
.pure-form input[type=url],
.pure-form input[type=date],
.pure-form input[type=month],
.pure-form input[type=time],
.pure-form input[type=datetime],
.pure-form input[type=datetime-local],
.pure-form input[type=week],
.pure-form input[type=tel],
.pure-form input[type=color],
.pure-form input[type=number],
.pure-form input[type=search],
.pure-form input[type=text],
.pure-form select,
.pure-form textarea {
padding: .5em .6em;
display: inline-block;
border: 1px solid #ccc;
box-shadow: inset 0 1px 3px #ddd;
border-radius: 4px;
vertical-align: middle;
box-sizing: border-box
}
.pure-form input:not([type]) {
padding: .5em .6em;
display: inline-block;
border: 1px solid #ccc;
box-shadow: inset 0 1px 3px #ddd;
border-radius: 4px
}
.pure-form input[type=color] {
padding: .2em .5em
}
.pure-form input:not([type]):focus,
.pure-form input[type=password]:focus,
.pure-form input[type=email]:focus,
.pure-form input[type=url]:focus,
.pure-form input[type=date]:focus,
.pure-form input[type=month]:focus,
.pure-form input[type=time]:focus,
.pure-form input[type=datetime]:focus,
.pure-form input[type=datetime-local]:focus,
.pure-form input[type=week]:focus,
.pure-form input[type=tel]:focus,
.pure-form input[type=color]:focus,
.pure-form input[type=number]:focus,
.pure-form input[type=search]:focus,
.pure-form input[type=text]:focus,
.pure-form select:focus,
.pure-form textarea:focus {
outline: 0;
border-color: #129FEA
}
.pure-form input[type=file]:focus,
.pure-form input[type=checkbox]:focus,
.pure-form input[type=radio]:focus {
outline: #129FEA auto 1px
}
.pure-form .pure-checkbox,
.pure-form .pure-radio {
margin: .5em 0;
display: block
}
.pure-form input:not([type])[disabled],
.pure-form input[type=password][disabled],
.pure-form input[type=email][disabled],
.pure-form input[type=url][disabled],
.pure-form input[type=date][disabled],
.pure-form input[type=month][disabled],
.pure-form input[type=time][disabled],
.pure-form input[type=datetime][disabled],
.pure-form input[type=datetime-local][disabled],
.pure-form input[type=week][disabled],
.pure-form input[type=tel][disabled],
.pure-form input[type=color][disabled],
.pure-form input[type=number][disabled],
.pure-form input[type=search][disabled],
.pure-form input[type=text][disabled],
.pure-form select[disabled],
.pure-form textarea[disabled] {
cursor: not-allowed;
background-color: #eaeded;
color: #cad2d3
}
.pure-form input[readonly],
.pure-form select[readonly],
.pure-form textarea[readonly] {
background-color: #eee;
color: #777;
border-color: #ccc
}
.pure-form input:focus:invalid,
.pure-form select:focus:invalid,
.pure-form textarea:focus:invalid {
color: #b94a48;
border-color: #e9322d
}
.pure-form input[type=file]:focus:invalid:focus,
.pure-form input[type=checkbox]:focus:invalid:focus,
.pure-form input[type=radio]:focus:invalid:focus {
outline-color: #e9322d
}
.pure-form select {
height: 2.25em;
border: 1px solid #ccc;
background-color: #fff
}
.pure-form select[multiple] {
height: auto
}
.pure-form label {
margin: .5em 0 .2em
}
.pure-form fieldset {
margin: 0;
padding: .35em 0 .75em;
border: 0
}
.pure-form legend {
display: block;
width: 100%;
padding: .3em 0;
margin-bottom: .3em;
color: #333;
border-bottom: 1px solid #e5e5e5
}
.pure-form-stacked input:not([type]),
.pure-form-stacked input[type=password],
.pure-form-stacked input[type=email],
.pure-form-stacked input[type=url],
.pure-form-stacked input[type=date],
.pure-form-stacked input[type=month],
.pure-form-stacked input[type=time],
.pure-form-stacked input[type=datetime],
.pure-form-stacked input[type=datetime-local],
.pure-form-stacked input[type=week],
.pure-form-stacked input[type=tel],
.pure-form-stacked input[type=color],
.pure-form-stacked input[type=file],
.pure-form-stacked input[type=number],
.pure-form-stacked input[type=search],
.pure-form-stacked input[type=text],
.pure-form-stacked label,
.pure-form-stacked select,
.pure-form-stacked textarea {
display: block;
margin: .25em 0
}
.pure-form-aligned .pure-help-inline,
.pure-form-aligned input,
.pure-form-aligned select,
.pure-form-aligned textarea,
.pure-form-message-inline {
display: inline-block;
vertical-align: middle
}
.pure-form-aligned textarea {
vertical-align: top
}
.pure-form-aligned .pure-control-group {
margin-bottom: .5em
}
.pure-form-aligned .pure-control-group label {
text-align: right;
display: inline-block;
vertical-align: middle;
width: 10em;
margin: 0 1em 0 0
}
.pure-form-aligned .pure-controls {
margin: 1.5em 0 0 11em
}
.pure-form .pure-input-rounded,
.pure-form input.pure-input-rounded {
border-radius: 2em;
padding: .5em 1em
}
.pure-form .pure-group fieldset {
margin-bottom: 10px
}
.pure-form .pure-group input,
.pure-form .pure-group textarea {
display: block;
padding: 10px;
margin: 0 0 -1px;
border-radius: 0;
position: relative;
top: -1px
}
.pure-form .pure-group input:focus,
.pure-form .pure-group textarea:focus {
z-index: 3
}
.pure-form .pure-group input:first-child,
.pure-form .pure-group textarea:first-child {
top: 1px;
border-radius: 4px 4px 0 0;
margin: 0
}
.pure-form .pure-group input:first-child:last-child,
.pure-form .pure-group textarea:first-child:last-child {
top: 1px;
border-radius: 4px;
margin: 0
}
.pure-form .pure-group input:last-child,
.pure-form .pure-group textarea:last-child {
top: -2px;
border-radius: 0 0 4px 4px;
margin: 0
}
.pure-form .pure-group button {
margin: .35em 0
}
.pure-form .pure-input-1 {
width: 100%
}
.pure-form .pure-input-3-4 {
width: 75%
}
.pure-form .pure-input-2-3 {
width: 66%
}
.pure-form .pure-input-1-2 {
width: 50%
}
.pure-form .pure-input-1-3 {
width: 33%
}
.pure-form .pure-input-1-4 {
width: 25%
}
.pure-form .pure-help-inline,
.pure-form-message-inline {
display: inline-block;
padding-left: .3em;
color: #666;
vertical-align: middle;
font-size: .875em
}
.pure-form-message {
display: block;
color: #666;
font-size: .875em
}
@media only screen and (max-width :480px) {
.pure-form button[type=submit] {
margin: .7em 0 0
}
.pure-form input:not([type]),
.pure-form input[type=password],
.pure-form input[type=email],
.pure-form input[type=url],
.pure-form input[type=date],
.pure-form input[type=month],
.pure-form input[type=time],
.pure-form input[type=datetime],
.pure-form input[type=datetime-local],
.pure-form input[type=week],
.pure-form input[type=tel],
.pure-form input[type=color],
.pure-form input[type=number],
.pure-form input[type=search],
.pure-form input[type=text],
.pure-form label {
margin-bottom: .3em;
display: block
}
.pure-group input:not([type]),
.pure-group input[type=password],
.pure-group input[type=email],
.pure-group input[type=url],
.pure-group input[type=date],
.pure-group input[type=month],
.pure-group input[type=time],
.pure-group input[type=datetime],
.pure-group input[type=datetime-local],
.pure-group input[type=week],
.pure-group input[type=tel],
.pure-group input[type=color],
.pure-group input[type=number],
.pure-group input[type=search],
.pure-group input[type=text] {
margin-bottom: 0
}
.pure-form-aligned .pure-control-group label {
margin-bottom: .3em;
text-align: left;
display: block;
width: 100%
}
.pure-form-aligned .pure-controls {
margin: 1.5em 0 0
}
.pure-form .pure-help-inline,
.pure-form-message,
.pure-form-message-inline {
display: block;
font-size: .75em;
padding: .2em 0 .8em
}
}
.pure-menu-fixed {
position: fixed;
left: 0;
top: 0;
z-index: 3
}
.pure-menu-item,
.pure-menu-list {
position: relative
}
.pure-menu-list {
list-style: none;
margin: 0;
padding: 0
}
.pure-menu-item {
padding: 0;
margin: 0;
height: 100%
}
.pure-menu-heading,
.pure-menu-link {
display: block;
text-decoration: none;
white-space: nowrap
}
.pure-menu-horizontal {
width: 100%;
white-space: nowrap
}
.pure-menu-horizontal .pure-menu-list {
display: inline-block
}
.pure-menu-horizontal .pure-menu-heading,
.pure-menu-horizontal .pure-menu-item,
.pure-menu-horizontal .pure-menu-separator {
display: inline-block;
zoom: 1;
vertical-align: middle
}
.pure-menu-item .pure-menu-item {
display: block
}
.pure-menu-children {
display: none;
position: absolute;
left: 100%;
top: 0;
margin: 0;
padding: 0;
z-index: 3
}
.pure-menu-horizontal .pure-menu-children {
left: 0;
top: auto;
width: inherit
}
.pure-menu-active>.pure-menu-children,
.pure-menu-allow-hover:hover>.pure-menu-children {
display: block;
position: absolute
}
.pure-menu-has-children>.pure-menu-link:after {
padding-left: .5em;
content: "\25B8";
font-size: small
}
.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after {
content: "\25BE"
}
.pure-menu-scrollable {
overflow-y: scroll;
overflow-x: hidden
}
.pure-menu-scrollable .pure-menu-list {
display: block
}
.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
display: inline-block
}
.pure-menu-horizontal.pure-menu-scrollable {
white-space: nowrap;
overflow-y: hidden;
overflow-x: auto;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
padding: .5em 0
}
.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar {
display: none
}
.pure-menu-horizontal .pure-menu-children .pure-menu-separator,
.pure-menu-separator {
background-color: #ccc;
height: 1px;
margin: .3em 0
}
.pure-menu-horizontal .pure-menu-separator {
width: 1px;
height: 1.3em;
margin: 0 .3em
}
.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
display: block;
width: auto
}
.pure-menu-heading {
text-transform: uppercase;
color: #565d64
}
.pure-menu-link {
color: #777
}
.pure-menu-children {
background-color: #fff
}
.pure-menu-disabled,
.pure-menu-heading,
.pure-menu-link {
padding: .5em 1em
}
.pure-menu-disabled {
opacity: .5
}
.pure-menu-disabled .pure-menu-link:hover {
background-color: transparent
}
.pure-menu-active>.pure-menu-link,
.pure-menu-link:focus,
.pure-menu-link:hover {
background-color: #eee
}
.pure-menu-selected .pure-menu-link,
.pure-menu-selected .pure-menu-link:visited {
color: #000
}
.pure-table {
empty-cells: show;
border: 1px solid #cbcbcb
}
.pure-table caption {
color: #000;
font: italic 85%/1 arial, sans-serif;
padding: 1em 0;
text-align: center
}
.pure-table td,
.pure-table th {
border-left: 1px solid #cbcbcb;
border-width: 0 0 0 1px;
font-size: inherit;
margin: 0;
overflow: visible;
padding: .5em 1em
}
.pure-table td:first-child,
.pure-table th:first-child {
border-left-width: 0
}
.pure-table thead {
background-color: #e0e0e0;
color: #000;
text-align: left;
vertical-align: bottom
}
.pure-table td {
background-color: transparent
}
.pure-table-odd td,
.pure-table-striped tr:nth-child(2n-1) td {
background-color: #f2f2f2
}
.pure-table-bordered td {
border-bottom: 1px solid #cbcbcb
}
.pure-table-bordered tbody>tr:last-child>td {
border-bottom-width: 0
}
.pure-table-horizontal td,
.pure-table-horizontal th {
border-width: 0 0 1px;
border-bottom: 1px solid #cbcbcb
}
.pure-table-horizontal tbody>tr:last-child>td {
border-bottom-width: 0
}
================================================
FILE: source/website/styles/vuetify-min.css
================================================
/*!
* Vuetify v1.0.3
* Forged by John Leider
* Released under the MIT License.
*/
@-webkit-keyframes a {
59% {
margin-left: 0
}
60%,
80% {
margin-left: 2px
}
70%,
90% {
margin-left: -2px
}
}
@keyframes a {
59% {
margin-left: 0
}
60%,
80% {
margin-left: 2px
}
70%,
90% {
margin-left: -2px
}
}
.black {
background-color: #000 !important;
border-color: #000 !important
}
.black--text {
color: #000 !important
}
.black--text input,
.black--text textarea {
caret-color: #000 !important
}
.black--after:after {
background: #000 !important
}
.white {
background-color: #fff !important;
border-color: #fff !important
}
.white--text {
color: #fff !important
}
.white--text input,
.white--text textarea {
caret-color: #fff !important
}
.white--after:after {
background: #fff !important
}
.transparent {
background-color: transparent !important;
border-color: transparent !important
}
.transparent--text {
color: transparent !important
}
.transparent--text input,
.transparent--text textarea {
caret-color: transparent !important
}
.transparent--after:after {
background: transparent !important
}
.red {
background-color: #f44336 !important;
border-color: #f44336 !important
}
.red--text {
color: #f44336 !important
}
.red--text input,
.red--text textarea {
caret-color: #f44336 !important
}
.red--after:after {
background: #f44336 !important
}
.red.lighten-5 {
border-color: #ffebee !important
}
.red.lighten-5,
.red.lighten-5--after:after {
background-color: #ffebee !important
}
.red--text.text--lighten-5 {
color: #ffebee !important
}
.red--text.text--lighten-5 input,
.red--text.text--lighten-5 textarea {
caret-color: #ffebee !important
}
.red.lighten-4 {
border-color: #ffcdd2 !important
}
.red.lighten-4,
.red.lighten-4--after:after {
background-color: #ffcdd2 !important
}
.red--text.text--lighten-4 {
color: #ffcdd2 !important
}
.red--text.text--lighten-4 input,
.red--text.text--lighten-4 textarea {
caret-color: #ffcdd2 !important
}
.red.lighten-3 {
border-color: #ef9a9a !important
}
.red.lighten-3,
.red.lighten-3--after:after {
background-color: #ef9a9a !important
}
.red--text.text--lighten-3 {
color: #ef9a9a !important
}
.red--text.text--lighten-3 input,
.red--text.text--lighten-3 textarea {
caret-color: #ef9a9a !important
}
.red.lighten-2 {
border-color: #e57373 !important
}
.red.lighten-2,
.red.lighten-2--after:after {
background-color: #e57373 !important
}
.red--text.text--lighten-2 {
color: #e57373 !important
}
.red--text.text--lighten-2 input,
.red--text.text--lighten-2 textarea {
caret-color: #e57373 !important
}
.red.lighten-1 {
border-color: #ef5350 !important
}
.red.lighten-1,
.red.lighten-1--after:after {
background-color: #ef5350 !important
}
.red--text.text--lighten-1 {
color: #ef5350 !important
}
.red--text.text--lighten-1 input,
.red--text.text--lighten-1 textarea {
caret-color: #ef5350 !important
}
.red.darken-1 {
border-color: #e53935 !important
}
.red.darken-1,
.red.darken-1--after:after {
background-color: #e53935 !important
}
.red--text.text--darken-1 {
color: #e53935 !important
}
.red--text.text--darken-1 input,
.red--text.text--darken-1 textarea {
caret-color: #e53935 !important
}
.red.darken-2 {
border-color: #d32f2f !important
}
.red.darken-2,
.red.darken-2--after:after {
background-color: #d32f2f !important
}
.red--text.text--darken-2 {
color: #d32f2f !important
}
.red--text.text--darken-2 input,
.red--text.text--darken-2 textarea {
caret-color: #d32f2f !important
}
.red.darken-3 {
border-color: #c62828 !important
}
.red.darken-3,
.red.darken-3--after:after {
background-color: #c62828 !important
}
.red--text.text--darken-3 {
color: #c62828 !important
}
.red--text.text--darken-3 input,
.red--text.text--darken-3 textarea {
caret-color: #c62828 !important
}
.red.darken-4 {
border-color: #b71c1c !important
}
.red.darken-4,
.red.darken-4--after:after {
background-color: #b71c1c !important
}
.red--text.text--darken-4 {
color: #b71c1c !important
}
.red--text.text--darken-4 input,
.red--text.text--darken-4 textarea {
caret-color: #b71c1c !important
}
.red.accent-1 {
border-color: #ff8a80 !important
}
.red.accent-1,
.red.accent-1--after:after {
background-color: #ff8a80 !important
}
.red--text.text--accent-1 {
color: #ff8a80 !important
}
.red--text.text--accent-1 input,
.red--text.text--accent-1 textarea {
caret-color: #ff8a80 !important
}
.red.accent-2 {
border-color: #ff5252 !important
}
.red.accent-2,
.red.accent-2--after:after {
background-color: #ff5252 !important
}
.red--text.text--accent-2 {
color: #ff5252 !important
}
.red--text.text--accent-2 input,
.red--text.text--accent-2 textarea {
caret-color: #ff5252 !important
}
.red.accent-3 {
border-color: #ff1744 !important
}
.red.accent-3,
.red.accent-3--after:after {
background-color: #ff1744 !important
}
.red--text.text--accent-3 {
color: #ff1744 !important
}
.red--text.text--accent-3 input,
.red--text.text--accent-3 textarea {
caret-color: #ff1744 !important
}
.red.accent-4 {
border-color: #d50000 !important
}
.red.accent-4,
.red.accent-4--after:after {
background-color: #d50000 !important
}
.red--text.text--accent-4 {
color: #d50000 !important
}
.red--text.text--accent-4 input,
.red--text.text--accent-4 textarea {
caret-color: #d50000 !important
}
.pink {
background-color: #e91e63 !important;
border-color: #e91e63 !important
}
.pink--text {
color: #e91e63 !important
}
.pink--text input,
.pink--text textarea {
caret-color: #e91e63 !important
}
.pink--after:after {
background: #e91e63 !important
}
.pink.lighten-5 {
border-color: #fce4ec !important
}
.pink.lighten-5,
.pink.lighten-5--after:after {
background-color: #fce4ec !important
}
.pink--text.text--lighten-5 {
color: #fce4ec !important
}
.pink--text.text--lighten-5 input,
.pink--text.text--lighten-5 textarea {
caret-color: #fce4ec !important
}
.pink.lighten-4 {
border-color: #f8bbd0 !important
}
.pink.lighten-4,
.pink.lighten-4--after:after {
background-color: #f8bbd0 !important
}
.pink--text.text--lighten-4 {
color: #f8bbd0 !important
}
.pink--text.text--lighten-4 input,
.pink--text.text--lighten-4 textarea {
caret-color: #f8bbd0 !important
}
.pink.lighten-3 {
border-color: #f48fb1 !important
}
.pink.lighten-3,
.pink.lighten-3--after:after {
background-color: #f48fb1 !important
}
.pink--text.text--lighten-3 {
color: #f48fb1 !important
}
.pink--text.text--lighten-3 input,
.pink--text.text--lighten-3 textarea {
caret-color: #f48fb1 !important
}
.pink.lighten-2 {
border-color: #f06292 !important
}
.pink.lighten-2,
.pink.lighten-2--after:after {
background-color: #f06292 !important
}
.pink--text.text--lighten-2 {
color: #f06292 !important
}
.pink--text.text--lighten-2 input,
.pink--text.text--lighten-2 textarea {
caret-color: #f06292 !important
}
.pink.lighten-1 {
border-color: #ec407a !important
}
.pink.lighten-1,
.pink.lighten-1--after:after {
background-color: #ec407a !important
}
.pink--text.text--lighten-1 {
color: #ec407a !important
}
.pink--text.text--lighten-1 input,
.pink--text.text--lighten-1 textarea {
caret-color: #ec407a !important
}
.pink.darken-1 {
border-color: #d81b60 !important
}
.pink.darken-1,
.pink.darken-1--after:after {
background-color: #d81b60 !important
}
.pink--text.text--darken-1 {
color: #d81b60 !important
}
.pink--text.text--darken-1 input,
.pink--text.text--darken-1 textarea {
caret-color: #d81b60 !important
}
.pink.darken-2 {
border-color: #c2185b !important
}
.pink.darken-2,
.pink.darken-2--after:after {
background-color: #c2185b !important
}
.pink--text.text--darken-2 {
color: #c2185b !important
}
.pink--text.text--darken-2 input,
.pink--text.text--darken-2 textarea {
caret-color: #c2185b !important
}
.pink.darken-3 {
border-color: #ad1457 !important
}
.pink.darken-3,
.pink.darken-3--after:after {
background-color: #ad1457 !important
}
.pink--text.text--darken-3 {
color: #ad1457 !important
}
.pink--text.text--darken-3 input,
.pink--text.text--darken-3 textarea {
caret-color: #ad1457 !important
}
.pink.darken-4 {
border-color: #880e4f !important
}
.pink.darken-4,
.pink.darken-4--after:after {
background-color: #880e4f !important
}
.pink--text.text--darken-4 {
color: #880e4f !important
}
.pink--text.text--darken-4 input,
.pink--text.text--darken-4 textarea {
caret-color: #880e4f !important
}
.pink.accent-1 {
border-color: #ff80ab !important
}
.pink.accent-1,
.pink.accent-1--after:after {
background-color: #ff80ab !important
}
.pink--text.text--accent-1 {
color: #ff80ab !important
}
.pink--text.text--accent-1 input,
.pink--text.text--accent-1 textarea {
caret-color: #ff80ab !important
}
.pink.accent-2 {
border-color: #ff4081 !important
}
.pink.accent-2,
.pink.accent-2--after:after {
background-color: #ff4081 !important
}
.pink--text.text--accent-2 {
color: #ff4081 !important
}
.pink--text.text--accent-2 input,
.pink--text.text--accent-2 textarea {
caret-color: #ff4081 !important
}
.pink.accent-3 {
border-color: #f50057 !important
}
.pink.accent-3,
.pink.accent-3--after:after {
background-color: #f50057 !important
}
.pink--text.text--accent-3 {
color: #f50057 !important
}
.pink--text.text--accent-3 input,
.pink--text.text--accent-3 textarea {
caret-color: #f50057 !important
}
.pink.accent-4 {
border-color: #c51162 !important
}
.pink.accent-4,
.pink.accent-4--after:after {
background-color: #c51162 !important
}
.pink--text.text--accent-4 {
color: #c51162 !important
}
.pink--text.text--accent-4 input,
.pink--text.text--accent-4 textarea {
caret-color: #c51162 !important
}
.purple {
background-color: #9c27b0 !important;
border-color: #9c27b0 !important
}
.purple--text {
color: #9c27b0 !important
}
.purple--text input,
.purple--text textarea {
caret-color: #9c27b0 !important
}
.purple--after:after {
background: #9c27b0 !important
}
.purple.lighten-5 {
border-color: #f3e5f5 !important
}
.purple.lighten-5,
.purple.lighten-5--after:after {
background-color: #f3e5f5 !important
}
.purple--text.text--lighten-5 {
color: #f3e5f5 !important
}
.purple--text.text--lighten-5 input,
.purple--text.text--lighten-5 textarea {
caret-color: #f3e5f5 !important
}
.purple.lighten-4 {
border-color: #e1bee7 !important
}
.purple.lighten-4,
.purple.lighten-4--after:after {
background-color: #e1bee7 !important
}
.purple--text.text--lighten-4 {
color: #e1bee7 !important
}
.purple--text.text--lighten-4 input,
.purple--text.text--lighten-4 textarea {
caret-color: #e1bee7 !important
}
.purple.lighten-3 {
border-color: #ce93d8 !important
}
.purple.lighten-3,
.purple.lighten-3--after:after {
background-color: #ce93d8 !important
}
.purple--text.text--lighten-3 {
color: #ce93d8 !important
}
.purple--text.text--lighten-3 input,
.purple--text.text--lighten-3 textarea {
caret-color: #ce93d8 !important
}
.purple.lighten-2 {
border-color: #ba68c8 !important
}
.purple.lighten-2,
.purple.lighten-2--after:after {
background-color: #ba68c8 !important
}
.purple--text.text--lighten-2 {
color: #ba68c8 !important
}
.purple--text.text--lighten-2 input,
.purple--text.text--lighten-2 textarea {
caret-color: #ba68c8 !important
}
.purple.lighten-1 {
border-color: #ab47bc !important
}
.purple.lighten-1,
.purple.lighten-1--after:after {
background-color: #ab47bc !important
}
.purple--text.text--lighten-1 {
color: #ab47bc !important
}
.purple--text.text--lighten-1 input,
.purple--text.text--lighten-1 textarea {
caret-color: #ab47bc !important
}
.purple.darken-1 {
border-color: #8e24aa !important
}
.purple.darken-1,
.purple.darken-1--after:after {
background-color: #8e24aa !important
}
.purple--text.text--darken-1 {
color: #8e24aa !important
}
.purple--text.text--darken-1 input,
.purple--text.text--darken-1 textarea {
caret-color: #8e24aa !important
}
.purple.darken-2 {
border-color: #7b1fa2 !important
}
.purple.darken-2,
.purple.darken-2--after:after {
background-color: #7b1fa2 !important
}
.purple--text.text--darken-2 {
color: #7b1fa2 !important
}
.purple--text.text--darken-2 input,
.purple--text.text--darken-2 textarea {
caret-color: #7b1fa2 !important
}
.purple.darken-3 {
border-color: #6a1b9a !important
}
.purple.darken-3,
.purple.darken-3--after:after {
background-color: #6a1b9a !important
}
.purple--text.text--darken-3 {
color: #6a1b9a !important
}
.purple--text.text--darken-3 input,
.purple--text.text--darken-3 textarea {
caret-color: #6a1b9a !important
}
.purple.darken-4 {
border-color: #4a148c !important
}
.purple.darken-4,
.purple.darken-4--after:after {
background-color: #4a148c !important
}
.purple--text.text--darken-4 {
color: #4a148c !important
}
.purple--text.text--darken-4 input,
.purple--text.text--darken-4 textarea {
caret-color: #4a148c !important
}
.purple.accent-1 {
border-color: #ea80fc !important
}
.purple.accent-1,
.purple.accent-1--after:after {
background-color: #ea80fc !important
}
.purple--text.text--accent-1 {
color: #ea80fc !important
}
.purple--text.text--accent-1 input,
.purple--text.text--accent-1 textarea {
caret-color: #ea80fc !important
}
.purple.accent-2 {
border-color: #e040fb !important
}
.purple.accent-2,
.purple.accent-2--after:after {
background-color: #e040fb !important
}
.purple--text.text--accent-2 {
color: #e040fb !important
}
.purple--text.text--accent-2 input,
.purple--text.text--accent-2 textarea {
caret-color: #e040fb !important
}
.purple.accent-3 {
border-color: #d500f9 !important
}
.purple.accent-3,
.purple.accent-3--after:after {
background-color: #d500f9 !important
}
.purple--text.text--accent-3 {
color: #d500f9 !important
}
.purple--text.text--accent-3 input,
.purple--text.text--accent-3 textarea {
caret-color: #d500f9 !important
}
.purple.accent-4 {
border-color: #a0f !important
}
.purple.accent-4,
.purple.accent-4--after:after {
background-color: #a0f !important
}
.purple--text.text--accent-4 {
color: #a0f !important
}
.purple--text.text--accent-4 input,
.purple--text.text--accent-4 textarea {
caret-color: #a0f !important
}
.deep-purple {
background-color: #673ab7 !important;
border-color: #673ab7 !important
}
.deep-purple--text {
color: #673ab7 !important
}
.deep-purple--text input,
.deep-purple--text textarea {
caret-color: #673ab7 !important
}
.deep-purple--after:after {
background: #673ab7 !important
}
.deep-purple.lighten-5 {
border-color: #ede7f6 !important
}
.deep-purple.lighten-5,
.deep-purple.lighten-5--after:after {
background-color: #ede7f6 !important
}
.deep-purple--text.text--lighten-5 {
color: #ede7f6 !important
}
.deep-purple--text.text--lighten-5 input,
.deep-purple--text.text--lighten-5 textarea {
caret-color: #ede7f6 !important
}
.deep-purple.lighten-4 {
border-color: #d1c4e9 !important
}
.deep-purple.lighten-4,
.deep-purple.lighten-4--after:after {
background-color: #d1c4e9 !important
}
.deep-purple--text.text--lighten-4 {
color: #d1c4e9 !important
}
.deep-purple--text.text--lighten-4 input,
.deep-purple--text.text--lighten-4 textarea {
caret-color: #d1c4e9 !important
}
.deep-purple.lighten-3 {
border-color: #b39ddb !important
}
.deep-purple.lighten-3,
.deep-purple.lighten-3--after:after {
background-color: #b39ddb !important
}
.deep-purple--text.text--lighten-3 {
color: #b39ddb !important
}
.deep-purple--text.text--lighten-3 input,
.deep-purple--text.text--lighten-3 textarea {
caret-color: #b39ddb !important
}
.deep-purple.lighten-2 {
border-color: #9575cd !important
}
.deep-purple.lighten-2,
.deep-purple.lighten-2--after:after {
background-color: #9575cd !important
}
.deep-purple--text.text--lighten-2 {
color: #9575cd !important
}
.deep-purple--text.text--lighten-2 input,
.deep-purple--text.text--lighten-2 textarea {
caret-color: #9575cd !important
}
.deep-purple.lighten-1 {
border-color: #7e57c2 !important
}
.deep-purple.lighten-1,
.deep-purple.lighten-1--after:after {
background-color: #7e57c2 !important
}
.deep-purple--text.text--lighten-1 {
color: #7e57c2 !important
}
.deep-purple--text.text--lighten-1 input,
.deep-purple--text.text--lighten-1 textarea {
caret-color: #7e57c2 !important
}
.deep-purple.darken-1 {
border-color: #5e35b1 !important
}
.deep-purple.darken-1,
.deep-purple.darken-1--after:after {
background-color: #5e35b1 !important
}
.deep-purple--text.text--darken-1 {
color: #5e35b1 !important
}
.deep-purple--text.text--darken-1 input,
.deep-purple--text.text--darken-1 textarea {
caret-color: #5e35b1 !important
}
.deep-purple.darken-2 {
border-color: #512da8 !important
}
.deep-purple.darken-2,
.deep-purple.darken-2--after:after {
background-color: #512da8 !important
}
.deep-purple--text.text--darken-2 {
color: #512da8 !important
}
.deep-purple--text.text--darken-2 input,
.deep-purple--text.text--darken-2 textarea {
caret-color: #512da8 !important
}
.deep-purple.darken-3 {
border-color: #4527a0 !important
}
.deep-purple.darken-3,
.deep-purple.darken-3--after:after {
background-color: #4527a0 !important
}
.deep-purple--text.text--darken-3 {
color: #4527a0 !important
}
.deep-purple--text.text--darken-3 input,
.deep-purple--text.text--darken-3 textarea {
caret-color: #4527a0 !important
}
.deep-purple.darken-4 {
border-color: #311b92 !important
}
.deep-purple.darken-4,
.deep-purple.darken-4--after:after {
background-color: #311b92 !important
}
.deep-purple--text.text--darken-4 {
color: #311b92 !important
}
.deep-purple--text.text--darken-4 input,
.deep-purple--text.text--darken-4 textarea {
caret-color: #311b92 !important
}
.deep-purple.accent-1 {
border-color: #b388ff !important
}
.deep-purple.accent-1,
.deep-purple.accent-1--after:after {
background-color: #b388ff !important
}
.deep-purple--text.text--accent-1 {
color: #b388ff !important
}
.deep-purple--text.text--accent-1 input,
.deep-purple--text.text--accent-1 textarea {
caret-color: #b388ff !important
}
.deep-purple.accent-2 {
border-color: #7c4dff !important
}
.deep-purple.accent-2,
.deep-purple.accent-2--after:after {
background-color: #7c4dff !important
}
.deep-purple--text.text--accent-2 {
color: #7c4dff !important
}
.deep-purple--text.text--accent-2 input,
.deep-purple--text.text--accent-2 textarea {
caret-color: #7c4dff !important
}
.deep-purple.accent-3 {
border-color: #651fff !important
}
.deep-purple.accent-3,
.deep-purple.accent-3--after:after {
background-color: #651fff !important
}
.deep-purple--text.text--accent-3 {
color: #651fff !important
}
.deep-purple--text.text--accent-3 input,
.deep-purple--text.text--accent-3 textarea {
caret-color: #651fff !important
}
.deep-purple.accent-4 {
border-color: #6200ea !important
}
.deep-purple.accent-4,
.deep-purple.accent-4--after:after {
background-color: #6200ea !important
}
.deep-purple--text.text--accent-4 {
color: #6200ea !important
}
.deep-purple--text.text--accent-4 input,
.deep-purple--text.text--accent-4 textarea {
caret-color: #6200ea !important
}
.indigo {
background-color: #3f51b5 !important;
border-color: #3f51b5 !important
}
.indigo--text {
color: #3f51b5 !important
}
.indigo--text input,
.indigo--text textarea {
caret-color: #3f51b5 !important
}
.indigo--after:after {
background: #3f51b5 !important
}
.indigo.lighten-5 {
border-color: #e8eaf6 !important
}
.indigo.lighten-5,
.indigo.lighten-5--after:after {
background-color: #e8eaf6 !important
}
.indigo--text.text--lighten-5 {
color: #e8eaf6 !important
}
.indigo--text.text--lighten-5 input,
.indigo--text.text--lighten-5 textarea {
caret-color: #e8eaf6 !important
}
.indigo.lighten-4 {
border-color: #c5cae9 !important
}
.indigo.lighten-4,
.indigo.lighten-4--after:after {
background-color: #c5cae9 !important
}
.indigo--text.text--lighten-4 {
color: #c5cae9 !important
}
.indigo--text.text--lighten-4 input,
.indigo--text.text--lighten-4 textarea {
caret-color: #c5cae9 !important
}
.indigo.lighten-3 {
border-color: #9fa8da !important
}
.indigo.lighten-3,
.indigo.lighten-3--after:after {
background-color: #9fa8da !important
}
.indigo--text.text--lighten-3 {
color: #9fa8da !important
}
.indigo--text.text--lighten-3 input,
.indigo--text.text--lighten-3 textarea {
caret-color: #9fa8da !important
}
.indigo.lighten-2 {
border-color: #7986cb !important
}
.indigo.lighten-2,
.indigo.lighten-2--after:after {
background-color: #7986cb !important
}
.indigo--text.text--lighten-2 {
color: #7986cb !important
}
.indigo--text.text--lighten-2 input,
.indigo--text.text--lighten-2 textarea {
caret-color: #7986cb !important
}
.indigo.lighten-1 {
border-color: #5c6bc0 !important
}
.indigo.lighten-1,
.indigo.lighten-1--after:after {
background-color: #5c6bc0 !important
}
.indigo--text.text--lighten-1 {
color: #5c6bc0 !important
}
.indigo--text.text--lighten-1 input,
.indigo--text.text--lighten-1 textarea {
caret-color: #5c6bc0 !important
}
.indigo.darken-1 {
border-color: #3949ab !important
}
.indigo.darken-1,
.indigo.darken-1--after:after {
background-color: #3949ab !important
}
.indigo--text.text--darken-1 {
color: #3949ab !important
}
.indigo--text.text--darken-1 input,
.indigo--text.text--darken-1 textarea {
caret-color: #3949ab !important
}
.indigo.darken-2 {
border-color: #303f9f !important
}
.indigo.darken-2,
.indigo.darken-2--after:after {
background-color: #303f9f !important
}
.indigo--text.text--darken-2 {
color: #303f9f !important
}
.indigo--text.text--darken-2 input,
.indigo--text.text--darken-2 textarea {
caret-color: #303f9f !important
}
.indigo.darken-3 {
border-color: #283593 !important
}
.indigo.darken-3,
.indigo.darken-3--after:after {
background-color: #283593 !important
}
.indigo--text.text--darken-3 {
color: #283593 !important
}
.indigo--text.text--darken-3 input,
.indigo--text.text--darken-3 textarea {
caret-color: #283593 !important
}
.indigo.darken-4 {
border-color: #1a237e !important
}
.indigo.darken-4,
.indigo.darken-4--after:after {
background-color: #1a237e !important
}
.indigo--text.text--darken-4 {
color: #1a237e !important
}
.indigo--text.text--darken-4 input,
.indigo--text.text--darken-4 textarea {
caret-color: #1a237e !important
}
.indigo.accent-1 {
border-color: #8c9eff !important
}
.indigo.accent-1,
.indigo.accent-1--after:after {
background-color: #8c9eff !important
}
.indigo--text.text--accent-1 {
color: #8c9eff !important
}
.indigo--text.text--accent-1 input,
.indigo--text.text--accent-1 textarea {
caret-color: #8c9eff !important
}
.indigo.accent-2 {
border-color: #536dfe !important
}
.indigo.accent-2,
.indigo.accent-2--after:after {
background-color: #536dfe !important
}
.indigo--text.text--accent-2 {
color: #536dfe !important
}
.indigo--text.text--accent-2 input,
.indigo--text.text--accent-2 textarea {
caret-color: #536dfe !important
}
.indigo.accent-3 {
border-color: #3d5afe !important
}
.indigo.accent-3,
.indigo.accent-3--after:after {
background-color: #3d5afe !important
}
.indigo--text.text--accent-3 {
color: #3d5afe !important
}
.indigo--text.text--accent-3 input,
.indigo--text.text--accent-3 textarea {
caret-color: #3d5afe !important
}
.indigo.accent-4 {
border-color: #304ffe !important
}
.indigo.accent-4,
.indigo.accent-4--after:after {
background-color: #304ffe !important
}
.indigo--text.text--accent-4 {
color: #304ffe !important
}
.indigo--text.text--accent-4 input,
.indigo--text.text--accent-4 textarea {
caret-color: #304ffe !important
}
.blue {
background-color: #2196f3 !important;
border-color: #2196f3 !important
}
.blue--text {
color: #2196f3 !important
}
.blue--text input,
.blue--text textarea {
caret-color: #2196f3 !important
}
.blue--after:after {
background: #2196f3 !important
}
.blue.lighten-5 {
border-color: #e3f2fd !important
}
.blue.lighten-5,
.blue.lighten-5--after:after {
background-color: #e3f2fd !important
}
.blue--text.text--lighten-5 {
color: #e3f2fd !important
}
.blue--text.text--lighten-5 input,
.blue--text.text--lighten-5 textarea {
caret-color: #e3f2fd !important
}
.blue.lighten-4 {
border-color: #bbdefb !important
}
.blue.lighten-4,
.blue.lighten-4--after:after {
background-color: #bbdefb !important
}
.blue--text.text--lighten-4 {
color: #bbdefb !important
}
.blue--text.text--lighten-4 input,
.blue--text.text--lighten-4 textarea {
caret-color: #bbdefb !important
}
.blue.lighten-3 {
border-color: #90caf9 !important
}
.blue.lighten-3,
.blue.lighten-3--after:after {
background-color: #90caf9 !important
}
.blue--text.text--lighten-3 {
color: #90caf9 !important
}
.blue--text.text--lighten-3 input,
.blue--text.text--lighten-3 textarea {
caret-color: #90caf9 !important
}
.blue.lighten-2 {
border-color: #64b5f6 !important
}
.blue.lighten-2,
.blue.lighten-2--after:after {
background-color: #64b5f6 !important
}
.blue--text.text--lighten-2 {
color: #64b5f6 !important
}
.blue--text.text--lighten-2 input,
.blue--text.text--lighten-2 textarea {
caret-color: #64b5f6 !important
}
.blue.lighten-1 {
border-color: #42a5f5 !important
}
.blue.lighten-1,
.blue.lighten-1--after:after {
background-color: #42a5f5 !important
}
.blue--text.text--lighten-1 {
color: #42a5f5 !important
}
.blue--text.text--lighten-1 input,
.blue--text.text--lighten-1 textarea {
caret-color: #42a5f5 !important
}
.blue.darken-1 {
border-color: #1e88e5 !important
}
.blue.darken-1,
.blue.darken-1--after:after {
background-color: #1e88e5 !important
}
.blue--text.text--darken-1 {
color: #1e88e5 !important
}
.blue--text.text--darken-1 input,
.blue--text.text--darken-1 textarea {
caret-color: #1e88e5 !important
}
.blue.darken-2 {
border-color: #1976d2 !important
}
.blue.darken-2,
.blue.darken-2--after:after {
background-color: #1976d2 !important
}
.blue--text.text--darken-2 {
color: #1976d2 !important
}
.blue--text.text--darken-2 input,
.blue--text.text--darken-2 textarea {
caret-color: #1976d2 !important
}
.blue.darken-3 {
border-color: #1565c0 !important
}
.blue.darken-3,
.blue.darken-3--after:after {
background-color: #1565c0 !important
}
.blue--text.text--darken-3 {
color: #1565c0 !important
}
.blue--text.text--darken-3 input,
.blue--text.text--darken-3 textarea {
caret-color: #1565c0 !important
}
.blue.darken-4 {
border-color: #0d47a1 !important
}
.blue.darken-4,
.blue.darken-4--after:after {
background-color: #0d47a1 !important
}
.blue--text.text--darken-4 {
color: #0d47a1 !important
}
.blue--text.text--darken-4 input,
.blue--text.text--darken-4 textarea {
caret-color: #0d47a1 !important
}
.blue.accent-1 {
border-color: #82b1ff !important
}
.blue.accent-1,
.blue.accent-1--after:after {
background-color: #82b1ff !important
}
.blue--text.text--accent-1 {
color: #82b1ff !important
}
.blue--text.text--accent-1 input,
.blue--text.text--accent-1 textarea {
caret-color: #82b1ff !important
}
.blue.accent-2 {
border-color: #448aff !important
}
.blue.accent-2,
.blue.accent-2--after:after {
background-color: #448aff !important
}
.blue--text.text--accent-2 {
color: #448aff !important
}
.blue--text.text--accent-2 input,
.blue--text.text--accent-2 textarea {
caret-color: #448aff !important
}
.blue.accent-3 {
border-color: #2979ff !important
}
.blue.accent-3,
.blue.accent-3--after:after {
background-color: #2979ff !important
}
.blue--text.text--accent-3 {
color: #2979ff !important
}
.blue--text.text--accent-3 input,
.blue--text.text--accent-3 textarea {
caret-color: #2979ff !important
}
.blue.accent-4 {
border-color: #2962ff !important
}
.blue.accent-4,
.blue.accent-4--after:after {
background-color: #2962ff !important
}
.blue--text.text--accent-4 {
color: #2962ff !important
}
.blue--text.text--accent-4 input,
.blue--text.text--accent-4 textarea {
caret-color: #2962ff !important
}
.light-blue {
background-color: #03a9f4 !important;
border-color: #03a9f4 !important
}
.light-blue--text {
color: #03a9f4 !important
}
.light-blue--text input,
.light-blue--text textarea {
caret-color: #03a9f4 !important
}
.light-blue--after:after {
background: #03a9f4 !important
}
.light-blue.lighten-5 {
border-color: #e1f5fe !important
}
.light-blue.lighten-5,
.light-blue.lighten-5--after:after {
background-color: #e1f5fe !important
}
.light-blue--text.text--lighten-5 {
color: #e1f5fe !important
}
.light-blue--text.text--lighten-5 input,
.light-blue--text.text--lighten-5 textarea {
caret-color: #e1f5fe !important
}
.light-blue.lighten-4 {
border-color: #b3e5fc !important
}
.light-blue.lighten-4,
.light-blue.lighten-4--after:after {
background-color: #b3e5fc !important
}
.light-blue--text.text--lighten-4 {
color: #b3e5fc !important
}
.light-blue--text.text--lighten-4 input,
.light-blue--text.text--lighten-4 textarea {
caret-color: #b3e5fc !important
}
.light-blue.lighten-3 {
border-color: #81d4fa !important
}
.light-blue.lighten-3,
.light-blue.lighten-3--after:after {
background-color: #81d4fa !important
}
.light-blue--text.text--lighten-3 {
color: #81d4fa !important
}
.light-blue--text.text--lighten-3 input,
.light-blue--text.text--lighten-3 textarea {
caret-color: #81d4fa !important
}
.light-blue.lighten-2 {
border-color: #4fc3f7 !important
}
.light-blue.lighten-2,
.light-blue.lighten-2--after:after {
background-color: #4fc3f7 !important
}
.light-blue--text.text--lighten-2 {
color: #4fc3f7 !important
}
.light-blue--text.text--lighten-2 input,
.light-blue--text.text--lighten-2 textarea {
caret-color: #4fc3f7 !important
}
.light-blue.lighten-1 {
border-color: #29b6f6 !important
}
.light-blue.lighten-1,
.light-blue.lighten-1--after:after {
background-color: #29b6f6 !important
}
.light-blue--text.text--lighten-1 {
color: #29b6f6 !important
}
.light-blue--text.text--lighten-1 input,
.light-blue--text.text--lighten-1 textarea {
caret-color: #29b6f6 !important
}
.light-blue.darken-1 {
border-color: #039be5 !important
}
.light-blue.darken-1,
.light-blue.darken-1--after:after {
background-color: #039be5 !important
}
.light-blue--text.text--darken-1 {
color: #039be5 !important
}
.light-blue--text.text--darken-1 input,
.light-blue--text.text--darken-1 textarea {
caret-color: #039be5 !important
}
.light-blue.darken-2 {
border-color: #0288d1 !important
}
.light-blue.darken-2,
.light-blue.darken-2--after:after {
background-color: #0288d1 !important
}
.light-blue--text.text--darken-2 {
color: #0288d1 !important
}
.light-blue--text.text--darken-2 input,
.light-blue--text.text--darken-2 textarea {
caret-color: #0288d1 !important
}
.light-blue.darken-3 {
border-color: #0277bd !important
}
.light-blue.darken-3,
.light-blue.darken-3--after:after {
background-color: #0277bd !important
}
.light-blue--text.text--darken-3 {
color: #0277bd !important
}
.light-blue--text.text--darken-3 input,
.light-blue--text.text--darken-3 textarea {
caret-color: #0277bd !important
}
.light-blue.darken-4 {
border-color: #01579b !important
}
.light-blue.darken-4,
.light-blue.darken-4--after:after {
background-color: #01579b !important
}
.light-blue--text.text--darken-4 {
color: #01579b !important
}
.light-blue--text.text--darken-4 input,
.light-blue--text.text--darken-4 textarea {
caret-color: #01579b !important
}
.light-blue.accent-1 {
border-color: #80d8ff !important
}
.light-blue.accent-1,
.light-blue.accent-1--after:after {
background-color: #80d8ff !important
}
.light-blue--text.text--accent-1 {
color: #80d8ff !important
}
.light-blue--text.text--accent-1 input,
.light-blue--text.text--accent-1 textarea {
caret-color: #80d8ff !important
}
.light-blue.accent-2 {
border-color: #40c4ff !important
}
.light-blue.accent-2,
.light-blue.accent-2--after:after {
background-color: #40c4ff !important
}
.light-blue--text.text--accent-2 {
color: #40c4ff !important
}
.light-blue--text.text--accent-2 input,
.light-blue--text.text--accent-2 textarea {
caret-color: #40c4ff !important
}
.light-blue.accent-3 {
border-color: #00b0ff !important
}
.light-blue.accent-3,
.light-blue.accent-3--after:after {
background-color: #00b0ff !important
}
.light-blue--text.text--accent-3 {
color: #00b0ff !important
}
.light-blue--text.text--accent-3 input,
.light-blue--text.text--accent-3 textarea {
caret-color: #00b0ff !important
}
.light-blue.accent-4 {
border-color: #0091ea !important
}
.light-blue.accent-4,
.light-blue.accent-4--after:after {
background-color: #0091ea !important
}
.light-blue--text.text--accent-4 {
color: #0091ea !important
}
.light-blue--text.text--accent-4 input,
.light-blue--text.text--accent-4 textarea {
caret-color: #0091ea !important
}
.cyan {
background-color: #00bcd4 !important;
border-color: #00bcd4 !important
}
.cyan--text {
color: #00bcd4 !important
}
.cyan--text input,
.cyan--text textarea {
caret-color: #00bcd4 !important
}
.cyan--after:after {
background: #00bcd4 !important
}
.cyan.lighten-5 {
border-color: #e0f7fa !important
}
.cyan.lighten-5,
.cyan.lighten-5--after:after {
background-color: #e0f7fa !important
}
.cyan--text.text--lighten-5 {
color: #e0f7fa !important
}
.cyan--text.text--lighten-5 input,
.cyan--text.text--lighten-5 textarea {
caret-color: #e0f7fa !important
}
.cyan.lighten-4 {
border-color: #b2ebf2 !important
}
.cyan.lighten-4,
.cyan.lighten-4--after:after {
background-color: #b2ebf2 !important
}
.cyan--text.text--lighten-4 {
color: #b2ebf2 !important
}
.cyan--text.text--lighten-4 input,
.cyan--text.text--lighten-4 textarea {
caret-color: #b2ebf2 !important
}
.cyan.lighten-3 {
border-color: #80deea !important
}
.cyan.lighten-3,
.cyan.lighten-3--after:after {
background-color: #80deea !important
}
.cyan--text.text--lighten-3 {
color: #80deea !important
}
.cyan--text.text--lighten-3 input,
.cyan--text.text--lighten-3 textarea {
caret-color: #80deea !important
}
.cyan.lighten-2 {
border-color: #4dd0e1 !important
}
.cyan.lighten-2,
.cyan.lighten-2--after:after {
background-color: #4dd0e1 !important
}
.cyan--text.text--lighten-2 {
color: #4dd0e1 !important
}
.cyan--text.text--lighten-2 input,
.cyan--text.text--lighten-2 textarea {
caret-color: #4dd0e1 !important
}
.cyan.lighten-1 {
border-color: #26c6da !important
}
.cyan.lighten-1,
.cyan.lighten-1--after:after {
background-color: #26c6da !important
}
.cyan--text.text--lighten-1 {
color: #26c6da !important
}
.cyan--text.text--lighten-1 input,
.cyan--text.text--lighten-1 textarea {
caret-color: #26c6da !important
}
.cyan.darken-1 {
border-color: #00acc1 !important
}
.cyan.darken-1,
.cyan.darken-1--after:after {
background-color: #00acc1 !important
}
.cyan--text.text--darken-1 {
color: #00acc1 !important
}
.cyan--text.text--darken-1 input,
.cyan--text.text--darken-1 textarea {
caret-color: #00acc1 !important
}
.cyan.darken-2 {
border-color: #0097a7 !important
}
.cyan.darken-2,
.cyan.darken-2--after:after {
background-color: #0097a7 !important
}
.cyan--text.text--darken-2 {
color: #0097a7 !important
}
.cyan--text.text--darken-2 input,
.cyan--text.text--darken-2 textarea {
caret-color: #0097a7 !important
}
.cyan.darken-3 {
border-color: #00838f !important
}
.cyan.darken-3,
.cyan.darken-3--after:after {
background-color: #00838f !important
}
.cyan--text.text--darken-3 {
color: #00838f !important
}
.cyan--text.text--darken-3 input,
.cyan--text.text--darken-3 textarea {
caret-color: #00838f !important
}
.cyan.darken-4 {
border-color: #006064 !important
}
.cyan.darken-4,
.cyan.darken-4--after:after {
background-color: #006064 !important
}
.cyan--text.text--darken-4 {
color: #006064 !important
}
.cyan--text.text--darken-4 input,
.cyan--text.text--darken-4 textarea {
caret-color: #006064 !important
}
.cyan.accent-1 {
border-color: #84ffff !important
}
.cyan.accent-1,
.cyan.accent-1--after:after {
background-color: #84ffff !important
}
.cyan--text.text--accent-1 {
color: #84ffff !important
}
.cyan--text.text--accent-1 input,
.cyan--text.text--accent-1 textarea {
caret-color: #84ffff !important
}
.cyan.accent-2 {
border-color: #18ffff !important
}
.cyan.accent-2,
.cyan.accent-2--after:after {
background-color: #18ffff !important
}
.cyan--text.text--accent-2 {
color: #18ffff !important
}
.cyan--text.text--accent-2 input,
.cyan--text.text--accent-2 textarea {
caret-color: #18ffff !important
}
.cyan.accent-3 {
border-color: #00e5ff !important
}
.cyan.accent-3,
.cyan.accent-3--after:after {
background-color: #00e5ff !important
}
.cyan--text.text--accent-3 {
color: #00e5ff !important
}
.cyan--text.text--accent-3 input,
.cyan--text.text--accent-3 textarea {
caret-color: #00e5ff !important
}
.cyan.accent-4 {
border-color: #00b8d4 !important
}
.cyan.accent-4,
.cyan.accent-4--after:after {
background-color: #00b8d4 !important
}
.cyan--text.text--accent-4 {
color: #00b8d4 !important
}
.cyan--text.text--accent-4 input,
.cyan--text.text--accent-4 textarea {
caret-color: #00b8d4 !important
}
.teal {
background-color: #009688 !important;
border-color: #009688 !important
}
.teal--text {
color: #009688 !important
}
.teal--text input,
.teal--text textarea {
caret-color: #009688 !important
}
.teal--after:after {
background: #009688 !important
}
.teal.lighten-5 {
border-color: #e0f2f1 !important
}
.teal.lighten-5,
.teal.lighten-5--after:after {
background-color: #e0f2f1 !important
}
.teal--text.text--lighten-5 {
color: #e0f2f1 !important
}
.teal--text.text--lighten-5 input,
.teal--text.text--lighten-5 textarea {
caret-color: #e0f2f1 !important
}
.teal.lighten-4 {
border-color: #b2dfdb !important
}
.teal.lighten-4,
.teal.lighten-4--after:after {
background-color: #b2dfdb !important
}
.teal--text.text--lighten-4 {
color: #b2dfdb !important
}
.teal--text.text--lighten-4 input,
.teal--text.text--lighten-4 textarea {
caret-color: #b2dfdb !important
}
.teal.lighten-3 {
border-color: #80cbc4 !important
}
.teal.lighten-3,
.teal.lighten-3--after:after {
background-color: #80cbc4 !important
}
.teal--text.text--lighten-3 {
color: #80cbc4 !important
}
.teal--text.text--lighten-3 input,
.teal--text.text--lighten-3 textarea {
caret-color: #80cbc4 !important
}
.teal.lighten-2 {
border-color: #4db6ac !important
}
.teal.lighten-2,
.teal.lighten-2--after:after {
background-color: #4db6ac !important
}
.teal--text.text--lighten-2 {
color: #4db6ac !important
}
.teal--text.text--lighten-2 input,
.teal--text.text--lighten-2 textarea {
caret-color: #4db6ac !important
}
.teal.lighten-1 {
border-color: #26a69a !important
}
.teal.lighten-1,
.teal.lighten-1--after:after {
background-color: #26a69a !important
}
.teal--text.text--lighten-1 {
color: #26a69a !important
}
.teal--text.text--lighten-1 input,
.teal--text.text--lighten-1 textarea {
caret-color: #26a69a !important
}
.teal.darken-1 {
border-color: #00897b !important
}
.teal.darken-1,
.teal.darken-1--after:after {
background-color: #00897b !important
}
.teal--text.text--darken-1 {
color: #00897b !important
}
.teal--text.text--darken-1 input,
.teal--text.text--darken-1 textarea {
caret-color: #00897b !important
}
.teal.darken-2 {
border-color: #00796b !important
}
.teal.darken-2,
.teal.darken-2--after:after {
background-color: #00796b !important
}
.teal--text.text--darken-2 {
color: #00796b !important
}
.teal--text.text--darken-2 input,
.teal--text.text--darken-2 textarea {
caret-color: #00796b !important
}
.teal.darken-3 {
border-color: #00695c !important
}
.teal.darken-3,
.teal.darken-3--after:after {
background-color: #00695c !important
}
.teal--text.text--darken-3 {
color: #00695c !important
}
.teal--text.text--darken-3 input,
.teal--text.text--darken-3 textarea {
caret-color: #00695c !important
}
.teal.darken-4 {
border-color: #004d40 !important
}
.teal.darken-4,
.teal.darken-4--after:after {
background-color: #004d40 !important
}
.teal--text.text--darken-4 {
color: #004d40 !important
}
.teal--text.text--darken-4 input,
.teal--text.text--darken-4 textarea {
caret-color: #004d40 !important
}
.teal.accent-1 {
border-color: #a7ffeb !important
}
.teal.accent-1,
.teal.accent-1--after:after {
background-color: #a7ffeb !important
}
.teal--text.text--accent-1 {
color: #a7ffeb !important
}
.teal--text.text--accent-1 input,
.teal--text.text--accent-1 textarea {
caret-color: #a7ffeb !important
}
.teal.accent-2 {
border-color: #64ffda !important
}
.teal.accent-2,
.teal.accent-2--after:after {
background-color: #64ffda !important
}
.teal--text.text--accent-2 {
color: #64ffda !important
}
.teal--text.text--accent-2 input,
.teal--text.text--accent-2 textarea {
caret-color: #64ffda !important
}
.teal.accent-3 {
border-color: #1de9b6 !important
}
.teal.accent-3,
.teal.accent-3--after:after {
background-color: #1de9b6 !important
}
.teal--text.text--accent-3 {
color: #1de9b6 !important
}
.teal--text.text--accent-3 input,
.teal--text.text--accent-3 textarea {
caret-color: #1de9b6 !important
}
.teal.accent-4 {
border-color: #00bfa5 !important
}
.teal.accent-4,
.teal.accent-4--after:after {
background-color: #00bfa5 !important
}
.teal--text.text--accent-4 {
color: #00bfa5 !important
}
.teal--text.text--accent-4 input,
.teal--text.text--accent-4 textarea {
caret-color: #00bfa5 !important
}
.green {
background-color: #4caf50 !important;
border-color: #4caf50 !important
}
.green--text {
color: #4caf50 !important
}
.green--text input,
.green--text textarea {
caret-color: #4caf50 !important
}
.green--after:after {
background: #4caf50 !important
}
.green.lighten-5 {
border-color: #e8f5e9 !important
}
.green.lighten-5,
.green.lighten-5--after:after {
background-color: #e8f5e9 !important
}
.green--text.text--lighten-5 {
color: #e8f5e9 !important
}
.green--text.text--lighten-5 input,
.green--text.text--lighten-5 textarea {
caret-color: #e8f5e9 !important
}
.green.lighten-4 {
border-color: #c8e6c9 !important
}
.green.lighten-4,
.green.lighten-4--after:after {
background-color: #c8e6c9 !important
}
.green--text.text--lighten-4 {
color: #c8e6c9 !important
}
.green--text.text--lighten-4 input,
.green--text.text--lighten-4 textarea {
caret-color: #c8e6c9 !important
}
.green.lighten-3 {
border-color: #a5d6a7 !important
}
.green.lighten-3,
.green.lighten-3--after:after {
background-color: #a5d6a7 !important
}
.green--text.text--lighten-3 {
color: #a5d6a7 !important
}
.green--text.text--lighten-3 input,
.green--text.text--lighten-3 textarea {
caret-color: #a5d6a7 !important
}
.green.lighten-2 {
border-color: #81c784 !important
}
.green.lighten-2,
.green.lighten-2--after:after {
background-color: #81c784 !important
}
.green--text.text--lighten-2 {
color: #81c784 !important
}
.green--text.text--lighten-2 input,
.green--text.text--lighten-2 textarea {
caret-color: #81c784 !important
}
.green.lighten-1 {
border-color: #66bb6a !important
}
.green.lighten-1,
.green.lighten-1--after:after {
background-color: #66bb6a !important
}
.green--text.text--lighten-1 {
color: #66bb6a !important
}
.green--text.text--lighten-1 input,
.green--text.text--lighten-1 textarea {
caret-color: #66bb6a !important
}
.green.darken-1 {
border-color: #43a047 !important
}
.green.darken-1,
.green.darken-1--after:after {
background-color: #43a047 !important
}
.green--text.text--darken-1 {
color: #43a047 !important
}
.green--text.text--darken-1 input,
.green--text.text--darken-1 textarea {
caret-color: #43a047 !important
}
.green.darken-2 {
border-color: #388e3c !important
}
.green.darken-2,
.green.darken-2--after:after {
background-color: #388e3c !important
}
.green--text.text--darken-2 {
color: #388e3c !important
}
.green--text.text--darken-2 input,
.green--text.text--darken-2 textarea {
caret-color: #388e3c !important
}
.green.darken-3 {
border-color: #2e7d32 !important
}
.green.darken-3,
.green.darken-3--after:after {
background-color: #2e7d32 !important
}
.green--text.text--darken-3 {
color: #2e7d32 !important
}
.green--text.text--darken-3 input,
.green--text.text--darken-3 textarea {
caret-color: #2e7d32 !important
}
.green.darken-4 {
border-color: #1b5e20 !important
}
.green.darken-4,
.green.darken-4--after:after {
background-color: #1b5e20 !important
}
.green--text.text--darken-4 {
color: #1b5e20 !important
}
.green--text.text--darken-4 input,
.green--text.text--darken-4 textarea {
caret-color: #1b5e20 !important
}
.green.accent-1 {
border-color: #b9f6ca !important
}
.green.accent-1,
.green.accent-1--after:after {
background-color: #b9f6ca !important
}
.green--text.text--accent-1 {
color: #b9f6ca !important
}
.green--text.text--accent-1 input,
.green--text.text--accent-1 textarea {
caret-color: #b9f6ca !important
}
.green.accent-2 {
border-color: #69f0ae !important
}
.green.accent-2,
.green.accent-2--after:after {
background-color: #69f0ae !important
}
.green--text.text--accent-2 {
color: #69f0ae !important
}
.green--text.text--accent-2 input,
.green--text.text--accent-2 textarea {
caret-color: #69f0ae !important
}
.green.accent-3 {
border-color: #00e676 !important
}
.green.accent-3,
.green.accent-3--after:after {
background-color: #00e676 !important
}
.green--text.text--accent-3 {
color: #00e676 !important
}
.green--text.text--accent-3 input,
.green--text.text--accent-3 textarea {
caret-color: #00e676 !important
}
.green.accent-4 {
border-color: #00c853 !important
}
.green.accent-4,
.green.accent-4--after:after {
background-color: #00c853 !important
}
.green--text.text--accent-4 {
color: #00c853 !important
}
.green--text.text--accent-4 input,
.green--text.text--accent-4 textarea {
caret-color: #00c853 !important
}
.light-green {
background-color: #8bc34a !important;
border-color: #8bc34a !important
}
.light-green--text {
color: #8bc34a !important
}
.light-green--text input,
.light-green--text textarea {
caret-color: #8bc34a !important
}
.light-green--after:after {
background: #8bc34a !important
}
.light-green.lighten-5 {
border-color: #f1f8e9 !important
}
.light-green.lighten-5,
.light-green.lighten-5--after:after {
background-color: #f1f8e9 !important
}
.light-green--text.text--lighten-5 {
color: #f1f8e9 !important
}
.light-green--text.text--lighten-5 input,
.light-green--text.text--lighten-5 textarea {
caret-color: #f1f8e9 !important
}
.light-green.lighten-4 {
border-color: #dcedc8 !important
}
.light-green.lighten-4,
.light-green.lighten-4--after:after {
background-color: #dcedc8 !important
}
.light-green--text.text--lighten-4 {
color: #dcedc8 !important
}
.light-green--text.text--lighten-4 input,
.light-green--text.text--lighten-4 textarea {
caret-color: #dcedc8 !important
}
.light-green.lighten-3 {
border-color: #c5e1a5 !important
}
.light-green.lighten-3,
.light-green.lighten-3--after:after {
background-color: #c5e1a5 !important
}
.light-green--text.text--lighten-3 {
color: #c5e1a5 !important
}
.light-green--text.text--lighten-3 input,
.light-green--text.text--lighten-3 textarea {
caret-color: #c5e1a5 !important
}
.light-green.lighten-2 {
border-color: #aed581 !important
}
.light-green.lighten-2,
.light-green.lighten-2--after:after {
background-color: #aed581 !important
}
.light-green--text.text--lighten-2 {
color: #aed581 !important
}
.light-green--text.text--lighten-2 input,
.light-green--text.text--lighten-2 textarea {
caret-color: #aed581 !important
}
.light-green.lighten-1 {
border-color: #9ccc65 !important
}
.light-green.lighten-1,
.light-green.lighten-1--after:after {
background-color: #9ccc65 !important
}
.light-green--text.text--lighten-1 {
color: #9ccc65 !important
}
.light-green--text.text--lighten-1 input,
.light-green--text.text--lighten-1 textarea {
caret-color: #9ccc65 !important
}
.light-green.darken-1 {
border-color: #7cb342 !important
}
.light-green.darken-1,
.light-green.darken-1--after:after {
background-color: #7cb342 !important
}
.light-green--text.text--darken-1 {
color: #7cb342 !important
}
.light-green--text.text--darken-1 input,
.light-green--text.text--darken-1 textarea {
caret-color: #7cb342 !important
}
.light-green.darken-2 {
border-color: #689f38 !important
}
.light-green.darken-2,
.light-green.darken-2--after:after {
background-color: #689f38 !important
}
.light-green--text.text--darken-2 {
color: #689f38 !important
}
.light-green--text.text--darken-2 input,
.light-green--text.text--darken-2 textarea {
caret-color: #689f38 !important
}
.light-green.darken-3 {
border-color: #558b2f !important
}
.light-green.darken-3,
.light-green.darken-3--after:after {
background-color: #558b2f !important
}
.light-green--text.text--darken-3 {
color: #558b2f !important
}
.light-green--text.text--darken-3 input,
.light-green--text.text--darken-3 textarea {
caret-color: #558b2f !important
}
.light-green.darken-4 {
border-color: #33691e !important
}
.light-green.darken-4,
.light-green.darken-4--after:after {
background-color: #33691e !important
}
.light-green--text.text--darken-4 {
color: #33691e !important
}
.light-green--text.text--darken-4 input,
.light-green--text.text--darken-4 textarea {
caret-color: #33691e !important
}
.light-green.accent-1 {
border-color: #ccff90 !important
}
.light-green.accent-1,
.light-green.accent-1--after:after {
background-color: #ccff90 !important
}
.light-green--text.text--accent-1 {
color: #ccff90 !important
}
.light-green--text.text--accent-1 input,
.light-green--text.text--accent-1 textarea {
caret-color: #ccff90 !important
}
.light-green.accent-2 {
border-color: #b2ff59 !important
}
.light-green.accent-2,
.light-green.accent-2--after:after {
background-color: #b2ff59 !important
}
.light-green--text.text--accent-2 {
color: #b2ff59 !important
}
.light-green--text.text--accent-2 input,
.light-green--text.text--accent-2 textarea {
caret-color: #b2ff59 !important
}
.light-green.accent-3 {
border-color: #76ff03 !important
}
.light-green.accent-3,
.light-green.accent-3--after:after {
background-color: #76ff03 !important
}
.light-green--text.text--accent-3 {
color: #76ff03 !important
}
.light-green--text.text--accent-3 input,
.light-green--text.text--accent-3 textarea {
caret-color: #76ff03 !important
}
.light-green.accent-4 {
border-color: #64dd17 !important
}
.light-green.accent-4,
.light-green.accent-4--after:after {
background-color: #64dd17 !important
}
.light-green--text.text--accent-4 {
color: #64dd17 !important
}
.light-green--text.text--accent-4 input,
.light-green--text.text--accent-4 textarea {
caret-color: #64dd17 !important
}
.lime {
background-color: #cddc39 !important;
border-color: #cddc39 !important
}
.lime--text {
color: #cddc39 !important
}
.lime--text input,
.lime--text textarea {
caret-color: #cddc39 !important
}
.lime--after:after {
background: #cddc39 !important
}
.lime.lighten-5 {
border-color: #f9fbe7 !important
}
.lime.lighten-5,
.lime.lighten-5--after:after {
background-color: #f9fbe7 !important
}
.lime--text.text--lighten-5 {
color: #f9fbe7 !important
}
.lime--text.text--lighten-5 input,
.lime--text.text--lighten-5 textarea {
caret-color: #f9fbe7 !important
}
.lime.lighten-4 {
border-color: #f0f4c3 !important
}
.lime.lighten-4,
.lime.lighten-4--after:after {
background-color: #f0f4c3 !important
}
.lime--text.text--lighten-4 {
color: #f0f4c3 !important
}
.lime--text.text--lighten-4 input,
.lime--text.text--lighten-4 textarea {
caret-color: #f0f4c3 !important
}
.lime.lighten-3 {
border-color: #e6ee9c !important
}
.lime.lighten-3,
.lime.lighten-3--after:after {
background-color: #e6ee9c !important
}
.lime--text.text--lighten-3 {
color: #e6ee9c !important
}
.lime--text.text--lighten-3 input,
.lime--text.text--lighten-3 textarea {
caret-color: #e6ee9c !important
}
.lime.lighten-2 {
border-color: #dce775 !important
}
.lime.lighten-2,
.lime.lighten-2--after:after {
background-color: #dce775 !important
}
.lime--text.text--lighten-2 {
color: #dce775 !important
}
.lime--text.text--lighten-2 input,
.lime--text.text--lighten-2 textarea {
caret-color: #dce775 !important
}
.lime.lighten-1 {
border-color: #d4e157 !important
}
.lime.lighten-1,
.lime.lighten-1--after:after {
background-color: #d4e157 !important
}
.lime--text.text--lighten-1 {
color: #d4e157 !important
}
.lime--text.text--lighten-1 input,
.lime--text.text--lighten-1 textarea {
caret-color: #d4e157 !important
}
.lime.darken-1 {
border-color: #c0ca33 !important
}
.lime.darken-1,
.lime.darken-1--after:after {
background-color: #c0ca33 !important
}
.lime--text.text--darken-1 {
color: #c0ca33 !important
}
.lime--text.text--darken-1 input,
.lime--text.text--darken-1 textarea {
caret-color: #c0ca33 !important
}
.lime.darken-2 {
border-color: #afb42b !important
}
.lime.darken-2,
.lime.darken-2--after:after {
background-color: #afb42b !important
}
.lime--text.text--darken-2 {
color: #afb42b !important
}
.lime--text.text--darken-2 input,
.lime--text.text--darken-2 textarea {
caret-color: #afb42b !important
}
.lime.darken-3 {
border-color: #9e9d24 !important
}
.lime.darken-3,
.lime.darken-3--after:after {
background-color: #9e9d24 !important
}
.lime--text.text--darken-3 {
color: #9e9d24 !important
}
.lime--text.text--darken-3 input,
.lime--text.text--darken-3 textarea {
caret-color: #9e9d24 !important
}
.lime.darken-4 {
border-color: #827717 !important
}
.lime.darken-4,
.lime.darken-4--after:after {
background-color: #827717 !important
}
.lime--text.text--darken-4 {
color: #827717 !important
}
.lime--text.text--darken-4 input,
.lime--text.text--darken-4 textarea {
caret-color: #827717 !important
}
.lime.accent-1 {
border-color: #f4ff81 !important
}
.lime.accent-1,
.lime.accent-1--after:after {
background-color: #f4ff81 !important
}
.lime--text.text--accent-1 {
color: #f4ff81 !important
}
.lime--text.text--accent-1 input,
.lime--text.text--accent-1 textarea {
caret-color: #f4ff81 !important
}
.lime.accent-2 {
border-color: #eeff41 !important
}
.lime.accent-2,
.lime.accent-2--after:after {
background-color: #eeff41 !important
}
.lime--text.text--accent-2 {
color: #eeff41 !important
}
.lime--text.text--accent-2 input,
.lime--text.text--accent-2 textarea {
caret-color: #eeff41 !important
}
.lime.accent-3 {
border-color: #c6ff00 !important
}
.lime.accent-3,
.lime.accent-3--after:after {
background-color: #c6ff00 !important
}
.lime--text.text--accent-3 {
color: #c6ff00 !important
}
.lime--text.text--accent-3 input,
.lime--text.text--accent-3 textarea {
caret-color: #c6ff00 !important
}
.lime.accent-4 {
border-color: #aeea00 !important
}
.lime.accent-4,
.lime.accent-4--after:after {
background-color: #aeea00 !important
}
.lime--text.text--accent-4 {
color: #aeea00 !important
}
.lime--text.text--accent-4 input,
.lime--text.text--accent-4 textarea {
caret-color: #aeea00 !important
}
.yellow {
background-color: #ffeb3b !important;
border-color: #ffeb3b !important
}
.yellow--text {
color: #ffeb3b !important
}
.yellow--text input,
.yellow--text textarea {
caret-color: #ffeb3b !important
}
.yellow--after:after {
background: #ffeb3b !important
}
.yellow.lighten-5 {
border-color: #fffde7 !important
}
.yellow.lighten-5,
.yellow.lighten-5--after:after {
background-color: #fffde7 !important
}
.yellow--text.text--lighten-5 {
color: #fffde7 !important
}
.yellow--text.text--lighten-5 input,
.yellow--text.text--lighten-5 textarea {
caret-color: #fffde7 !important
}
.yellow.lighten-4 {
border-color: #fff9c4 !important
}
.yellow.lighten-4,
.yellow.lighten-4--after:after {
background-color: #fff9c4 !important
}
.yellow--text.text--lighten-4 {
color: #fff9c4 !important
}
.yellow--text.text--lighten-4 input,
.yellow--text.text--lighten-4 textarea {
caret-color: #fff9c4 !important
}
.yellow.lighten-3 {
border-color: #fff59d !important
}
.yellow.lighten-3,
.yellow.lighten-3--after:after {
background-color: #fff59d !important
}
.yellow--text.text--lighten-3 {
color: #fff59d !important
}
.yellow--text.text--lighten-3 input,
.yellow--text.text--lighten-3 textarea {
caret-color: #fff59d !important
}
.yellow.lighten-2 {
border-color: #fff176 !important
}
.yellow.lighten-2,
.yellow.lighten-2--after:after {
background-color: #fff176 !important
}
.yellow--text.text--lighten-2 {
color: #fff176 !important
}
.yellow--text.text--lighten-2 input,
.yellow--text.text--lighten-2 textarea {
caret-color: #fff176 !important
}
.yellow.lighten-1 {
border-color: #ffee58 !important
}
.yellow.lighten-1,
.yellow.lighten-1--after:after {
background-color: #ffee58 !important
}
.yellow--text.text--lighten-1 {
color: #ffee58 !important
}
.yellow--text.text--lighten-1 input,
.yellow--text.text--lighten-1 textarea {
caret-color: #ffee58 !important
}
.yellow.darken-1 {
border-color: #fdd835 !important
}
.yellow.darken-1,
.yellow.darken-1--after:after {
background-color: #fdd835 !important
}
.yellow--text.text--darken-1 {
color: #fdd835 !important
}
.yellow--text.text--darken-1 input,
.yellow--text.text--darken-1 textarea {
caret-color: #fdd835 !important
}
.yellow.darken-2 {
border-color: #fbc02d !important
}
.yellow.darken-2,
.yellow.darken-2--after:after {
background-color: #fbc02d !important
}
.yellow--text.text--darken-2 {
color: #fbc02d !important
}
.yellow--text.text--darken-2 input,
.yellow--text.text--darken-2 textarea {
caret-color: #fbc02d !important
}
.yellow.darken-3 {
border-color: #f9a825 !important
}
.yellow.darken-3,
.yellow.darken-3--after:after {
background-color: #f9a825 !important
}
.yellow--text.text--darken-3 {
color: #f9a825 !important
}
.yellow--text.text--darken-3 input,
.yellow--text.text--darken-3 textarea {
caret-color: #f9a825 !important
}
.yellow.darken-4 {
border-color: #f57f17 !important
}
.yellow.darken-4,
.yellow.darken-4--after:after {
background-color: #f57f17 !important
}
.yellow--text.text--darken-4 {
color: #f57f17 !important
}
.yellow--text.text--darken-4 input,
.yellow--text.text--darken-4 textarea {
caret-color: #f57f17 !important
}
.yellow.accent-1 {
border-color: #ffff8d !important
}
.yellow.accent-1,
.yellow.accent-1--after:after {
background-color: #ffff8d !important
}
.yellow--text.text--accent-1 {
color: #ffff8d !important
}
.yellow--text.text--accent-1 input,
.yellow--text.text--accent-1 textarea {
caret-color: #ffff8d !important
}
.yellow.accent-2 {
border-color: #ff0 !important
}
.yellow.accent-2,
.yellow.accent-2--after:after {
background-color: #ff0 !important
}
.yellow--text.text--accent-2 {
color: #ff0 !important
}
.yellow--text.text--accent-2 input,
.yellow--text.text--accent-2 textarea {
caret-color: #ff0 !important
}
.yellow.accent-3 {
border-color: #ffea00 !important
}
.yellow.accent-3,
.yellow.accent-3--after:after {
background-color: #ffea00 !important
}
.yellow--text.text--accent-3 {
color: #ffea00 !important
}
.yellow--text.text--accent-3 input,
.yellow--text.text--accent-3 textarea {
caret-color: #ffea00 !important
}
.yellow.accent-4 {
border-color: #ffd600 !important
}
.yellow.accent-4,
.yellow.accent-4--after:after {
background-color: #ffd600 !important
}
.yellow--text.text--accent-4 {
color: #ffd600 !important
}
.yellow--text.text--accent-4 input,
.yellow--text.text--accent-4 textarea {
caret-color: #ffd600 !important
}
.amber {
background-color: #ffc107 !important;
border-color: #ffc107 !important
}
.amber--text {
color: #ffc107 !important
}
.amber--text input,
.amber--text textarea {
caret-color: #ffc107 !important
}
.amber--after:after {
background: #ffc107 !important
}
.amber.lighten-5 {
border-color: #fff8e1 !important
}
.amber.lighten-5,
.amber.lighten-5--after:after {
background-color: #fff8e1 !important
}
.amber--text.text--lighten-5 {
color: #fff8e1 !important
}
.amber--text.text--lighten-5 input,
.amber--text.text--lighten-5 textarea {
caret-color: #fff8e1 !important
}
.amber.lighten-4 {
border-color: #ffecb3 !important
}
.amber.lighten-4,
.amber.lighten-4--after:after {
background-color: #ffecb3 !important
}
.amber--text.text--lighten-4 {
color: #ffecb3 !important
}
.amber--text.text--lighten-4 input,
.amber--text.text--lighten-4 textarea {
caret-color: #ffecb3 !important
}
.amber.lighten-3 {
border-color: #ffe082 !important
}
.amber.lighten-3,
.amber.lighten-3--after:after {
background-color: #ffe082 !important
}
.amber--text.text--lighten-3 {
color: #ffe082 !important
}
.amber--text.text--lighten-3 input,
.amber--text.text--lighten-3 textarea {
caret-color: #ffe082 !important
}
.amber.lighten-2 {
border-color: #ffd54f !important
}
.amber.lighten-2,
.amber.lighten-2--after:after {
background-color: #ffd54f !important
}
.amber--text.text--lighten-2 {
color: #ffd54f !important
}
.amber--text.text--lighten-2 input,
.amber--text.text--lighten-2 textarea {
caret-color: #ffd54f !important
}
.amber.lighten-1 {
border-color: #ffca28 !important
}
.amber.lighten-1,
.amber.lighten-1--after:after {
background-color: #ffca28 !important
}
.amber--text.text--lighten-1 {
color: #ffca28 !important
}
.amber--text.text--lighten-1 input,
.amber--text.text--lighten-1 textarea {
caret-color: #ffca28 !important
}
.amber.darken-1 {
border-color: #ffb300 !important
}
.amber.darken-1,
.amber.darken-1--after:after {
background-color: #ffb300 !important
}
.amber--text.text--darken-1 {
color: #ffb300 !important
}
.amber--text.text--darken-1 input,
.amber--text.text--darken-1 textarea {
caret-color: #ffb300 !important
}
.amber.darken-2 {
border-color: #ffa000 !important
}
.amber.darken-2,
.amber.darken-2--after:after {
background-color: #ffa000 !important
}
.amber--text.text--darken-2 {
color: #ffa000 !important
}
.amber--text.text--darken-2 input,
.amber--text.text--darken-2 textarea {
caret-color: #ffa000 !important
}
.amber.darken-3 {
border-color: #ff8f00 !important
}
.amber.darken-3,
.amber.darken-3--after:after {
background-color: #ff8f00 !important
}
.amber--text.text--darken-3 {
color: #ff8f00 !important
}
.amber--text.text--darken-3 input,
.amber--text.text--darken-3 textarea {
caret-color: #ff8f00 !important
}
.amber.darken-4 {
border-color: #ff6f00 !important
}
.amber.darken-4,
.amber.darken-4--after:after {
background-color: #ff6f00 !important
}
.amber--text.text--darken-4 {
color: #ff6f00 !important
}
.amber--text.text--darken-4 input,
.amber--text.text--darken-4 textarea {
caret-color: #ff6f00 !important
}
.amber.accent-1 {
border-color: #ffe57f !important
}
.amber.accent-1,
.amber.accent-1--after:after {
background-color: #ffe57f !important
}
.amber--text.text--accent-1 {
color: #ffe57f !important
}
.amber--text.text--accent-1 input,
.amber--text.text--accent-1 textarea {
caret-color: #ffe57f !important
}
.amber.accent-2 {
border-color: #ffd740 !important
}
.amber.accent-2,
.amber.accent-2--after:after {
background-color: #ffd740 !important
}
.amber--text.text--accent-2 {
color: #ffd740 !important
}
.amber--text.text--accent-2 input,
.amber--text.text--accent-2 textarea {
caret-color: #ffd740 !important
}
.amber.accent-3 {
border-color: #ffc400 !important
}
.amber.accent-3,
.amber.accent-3--after:after {
background-color: #ffc400 !important
}
.amber--text.text--accent-3 {
color: #ffc400 !important
}
.amber--text.text--accent-3 input,
.amber--text.text--accent-3 textarea {
caret-color: #ffc400 !important
}
.amber.accent-4 {
border-color: #ffab00 !important
}
.amber.accent-4,
.amber.accent-4--after:after {
background-color: #ffab00 !important
}
.amber--text.text--accent-4 {
color: #ffab00 !important
}
.amber--text.text--accent-4 input,
.amber--text.text--accent-4 textarea {
caret-color: #ffab00 !important
}
.orange {
background-color: #ff9800 !important;
border-color: #ff9800 !important
}
.orange--text {
color: #ff9800 !important
}
.orange--text input,
.orange--text textarea {
caret-color: #ff9800 !important
}
.orange--after:after {
background: #ff9800 !important
}
.orange.lighten-5 {
border-color: #fff3e0 !important
}
.orange.lighten-5,
.orange.lighten-5--after:after {
background-color: #fff3e0 !important
}
.orange--text.text--lighten-5 {
color: #fff3e0 !important
}
.orange--text.text--lighten-5 input,
.orange--text.text--lighten-5 textarea {
caret-color: #fff3e0 !important
}
.orange.lighten-4 {
border-color: #ffe0b2 !important
}
.orange.lighten-4,
.orange.lighten-4--after:after {
background-color: #ffe0b2 !important
}
.orange--text.text--lighten-4 {
color: #ffe0b2 !important
}
.orange--text.text--lighten-4 input,
.orange--text.text--lighten-4 textarea {
caret-color: #ffe0b2 !important
}
.orange.lighten-3 {
border-color: #ffcc80 !important
}
.orange.lighten-3,
.orange.lighten-3--after:after {
background-color: #ffcc80 !important
}
.orange--text.text--lighten-3 {
color: #ffcc80 !important
}
.orange--text.text--lighten-3 input,
.orange--text.text--lighten-3 textarea {
caret-color: #ffcc80 !important
}
.orange.lighten-2 {
border-color: #ffb74d !important
}
.orange.lighten-2,
.orange.lighten-2--after:after {
background-color: #ffb74d !important
}
.orange--text.text--lighten-2 {
color: #ffb74d !important
}
.orange--text.text--lighten-2 input,
.orange--text.text--lighten-2 textarea {
caret-color: #ffb74d !important
}
.orange.lighten-1 {
border-color: #ffa726 !important
}
.orange.lighten-1,
.orange.lighten-1--after:after {
background-color: #ffa726 !important
}
.orange--text.text--lighten-1 {
color: #ffa726 !important
}
.orange--text.text--lighten-1 input,
.orange--text.text--lighten-1 textarea {
caret-color: #ffa726 !important
}
.orange.darken-1 {
border-color: #fb8c00 !important
}
.orange.darken-1,
.orange.darken-1--after:after {
background-color: #fb8c00 !important
}
.orange--text.text--darken-1 {
color: #fb8c00 !important
}
.orange--text.text--darken-1 input,
.orange--text.text--darken-1 textarea {
caret-color: #fb8c00 !important
}
.orange.darken-2 {
border-color: #f57c00 !important
}
.orange.darken-2,
.orange.darken-2--after:after {
background-color: #f57c00 !important
}
.orange--text.text--darken-2 {
color: #f57c00 !important
}
.orange--text.text--darken-2 input,
.orange--text.text--darken-2 textarea {
caret-color: #f57c00 !important
}
.orange.darken-3 {
border-color: #ef6c00 !important
}
.orange.darken-3,
.orange.darken-3--after:after {
background-color: #ef6c00 !important
}
.orange--text.text--darken-3 {
color: #ef6c00 !important
}
.orange--text.text--darken-3 input,
.orange--text.text--darken-3 textarea {
caret-color: #ef6c00 !important
}
.orange.darken-4 {
border-color: #e65100 !important
}
.orange.darken-4,
.orange.darken-4--after:after {
background-color: #e65100 !important
}
.orange--text.text--darken-4 {
color: #e65100 !important
}
.orange--text.text--darken-4 input,
.orange--text.text--darken-4 textarea {
caret-color: #e65100 !important
}
.orange.accent-1 {
border-color: #ffd180 !important
}
.orange.accent-1,
.orange.accent-1--after:after {
background-color: #ffd180 !important
}
.orange--text.text--accent-1 {
color: #ffd180 !important
}
.orange--text.text--accent-1 input,
.orange--text.text--accent-1 textarea {
caret-color: #ffd180 !important
}
.orange.accent-2 {
border-color: #ffab40 !important
}
.orange.accent-2,
.orange.accent-2--after:after {
background-color: #ffab40 !important
}
.orange--text.text--accent-2 {
color: #ffab40 !important
}
.orange--text.text--accent-2 input,
.orange--text.text--accent-2 textarea {
caret-color: #ffab40 !important
}
.orange.accent-3 {
border-color: #ff9100 !important
}
.orange.accent-3,
.orange.accent-3--after:after {
background-color: #ff9100 !important
}
.orange--text.text--accent-3 {
color: #ff9100 !important
}
.orange--text.text--accent-3 input,
.orange--text.text--accent-3 textarea {
caret-color: #ff9100 !important
}
.orange.accent-4 {
border-color: #ff6d00 !important
}
.orange.accent-4,
.orange.accent-4--after:after {
background-color: #ff6d00 !important
}
.orange--text.text--accent-4 {
color: #ff6d00 !important
}
.orange--text.text--accent-4 input,
.orange--text.text--accent-4 textarea {
caret-color: #ff6d00 !important
}
.deep-orange {
background-color: #ff5722 !important;
border-color: #ff5722 !important
}
.deep-orange--text {
color: #ff5722 !important
}
.deep-orange--text input,
.deep-orange--text textarea {
caret-color: #ff5722 !important
}
.deep-orange--after:after {
background: #ff5722 !important
}
.deep-orange.lighten-5 {
border-color: #fbe9e7 !important
}
.deep-orange.lighten-5,
.deep-orange.lighten-5--after:after {
background-color: #fbe9e7 !important
}
.deep-orange--text.text--lighten-5 {
color: #fbe9e7 !important
}
.deep-orange--text.text--lighten-5 input,
.deep-orange--text.text--lighten-5 textarea {
caret-color: #fbe9e7 !important
}
.deep-orange.lighten-4 {
border-color: #ffccbc !important
}
.deep-orange.lighten-4,
.deep-orange.lighten-4--after:after {
background-color: #ffccbc !important
}
.deep-orange--text.text--lighten-4 {
color: #ffccbc !important
}
.deep-orange--text.text--lighten-4 input,
.deep-orange--text.text--lighten-4 textarea {
caret-color: #ffccbc !important
}
.deep-orange.lighten-3 {
border-color: #ffab91 !important
}
.deep-orange.lighten-3,
.deep-orange.lighten-3--after:after {
background-color: #ffab91 !important
}
.deep-orange--text.text--lighten-3 {
color: #ffab91 !important
}
.deep-orange--text.text--lighten-3 input,
.deep-orange--text.text--lighten-3 textarea {
caret-color: #ffab91 !important
}
.deep-orange.lighten-2 {
border-color: #ff8a65 !important
}
.deep-orange.lighten-2,
.deep-orange.lighten-2--after:after {
background-color: #ff8a65 !important
}
.deep-orange--text.text--lighten-2 {
color: #ff8a65 !important
}
.deep-orange--text.text--lighten-2 input,
.deep-orange--text.text--lighten-2 textarea {
caret-color: #ff8a65 !important
}
.deep-orange.lighten-1 {
border-color: #ff7043 !important
}
.deep-orange.lighten-1,
.deep-orange.lighten-1--after:after {
background-color: #ff7043 !important
}
.deep-orange--text.text--lighten-1 {
color: #ff7043 !important
}
.deep-orange--text.text--lighten-1 input,
.deep-orange--text.text--lighten-1 textarea {
caret-color: #ff7043 !important
}
.deep-orange.darken-1 {
border-color: #f4511e !important
}
.deep-orange.darken-1,
.deep-orange.darken-1--after:after {
background-color: #f4511e !important
}
.deep-orange--text.text--darken-1 {
color: #f4511e !important
}
.deep-orange--text.text--darken-1 input,
.deep-orange--text.text--darken-1 textarea {
caret-color: #f4511e !important
}
.deep-orange.darken-2 {
border-color: #e64a19 !important
}
.deep-orange.darken-2,
.deep-orange.darken-2--after:after {
background-color: #e64a19 !important
}
.deep-orange--text.text--darken-2 {
color: #e64a19 !important
}
.deep-orange--text.text--darken-2 input,
.deep-orange--text.text--darken-2 textarea {
caret-color: #e64a19 !important
}
.deep-orange.darken-3 {
border-color: #d84315 !important
}
.deep-orange.darken-3,
.deep-orange.darken-3--after:after {
background-color: #d84315 !important
}
.deep-orange--text.text--darken-3 {
color: #d84315 !important
}
.deep-orange--text.text--darken-3 input,
.deep-orange--text.text--darken-3 textarea {
caret-color: #d84315 !important
}
.deep-orange.darken-4 {
border-color: #bf360c !important
}
.deep-orange.darken-4,
.deep-orange.darken-4--after:after {
background-color: #bf360c !important
}
.deep-orange--text.text--darken-4 {
color: #bf360c !important
}
.deep-orange--text.text--darken-4 input,
.deep-orange--text.text--darken-4 textarea {
caret-color: #bf360c !important
}
.deep-orange.accent-1 {
border-color: #ff9e80 !important
}
.deep-orange.accent-1,
.deep-orange.accent-1--after:after {
background-color: #ff9e80 !important
}
.deep-orange--text.text--accent-1 {
color: #ff9e80 !important
}
.deep-orange--text.text--accent-1 input,
.deep-orange--text.text--accent-1 textarea {
caret-color: #ff9e80 !important
}
.deep-orange.accent-2 {
border-color: #ff6e40 !important
}
.deep-orange.accent-2,
.deep-orange.accent-2--after:after {
background-color: #ff6e40 !important
}
.deep-orange--text.text--accent-2 {
color: #ff6e40 !important
}
.deep-orange--text.text--accent-2 input,
.deep-orange--text.text--accent-2 textarea {
caret-color: #ff6e40 !important
}
.deep-orange.accent-3 {
border-color: #ff3d00 !important
}
.deep-orange.accent-3,
.deep-orange.accent-3--after:after {
background-color: #ff3d00 !important
}
.deep-orange--text.text--accent-3 {
color: #ff3d00 !important
}
.deep-orange--text.text--accent-3 input,
.deep-orange--text.text--accent-3 textarea {
caret-color: #ff3d00 !important
}
.deep-orange.accent-4 {
border-color: #dd2c00 !important
}
.deep-orange.accent-4,
.deep-orange.accent-4--after:after {
background-color: #dd2c00 !important
}
.deep-orange--text.text--accent-4 {
color: #dd2c00 !important
}
.deep-orange--text.text--accent-4 input,
.deep-orange--text.text--accent-4 textarea {
caret-color: #dd2c00 !important
}
.brown {
background-color: #795548 !important;
border-color: #795548 !important
}
.brown--text {
color: #795548 !important
}
.brown--text input,
.brown--text textarea {
caret-color: #795548 !important
}
.brown--after:after {
background: #795548 !important
}
.brown.lighten-5 {
border-color: #efebe9 !important
}
.brown.lighten-5,
.brown.lighten-5--after:after {
background-color: #efebe9 !important
}
.brown--text.text--lighten-5 {
color: #efebe9 !important
}
.brown--text.text--lighten-5 input,
.brown--text.text--lighten-5 textarea {
caret-color: #efebe9 !important
}
.brown.lighten-4 {
border-color: #d7ccc8 !important
}
.brown.lighten-4,
.brown.lighten-4--after:after {
background-color: #d7ccc8 !important
}
.brown--text.text--lighten-4 {
color: #d7ccc8 !important
}
.brown--text.text--lighten-4 input,
.brown--text.text--lighten-4 textarea {
caret-color: #d7ccc8 !important
}
.brown.lighten-3 {
border-color: #bcaaa4 !important
}
.brown.lighten-3,
.brown.lighten-3--after:after {
background-color: #bcaaa4 !important
}
.brown--text.text--lighten-3 {
color: #bcaaa4 !important
}
.brown--text.text--lighten-3 input,
.brown--text.text--lighten-3 textarea {
caret-color: #bcaaa4 !important
}
.brown.lighten-2 {
border-color: #a1887f !important
}
.brown.lighten-2,
.brown.lighten-2--after:after {
background-color: #a1887f !important
}
.brown--text.text--lighten-2 {
color: #a1887f !important
}
.brown--text.text--lighten-2 input,
.brown--text.text--lighten-2 textarea {
caret-color: #a1887f !important
}
.brown.lighten-1 {
border-color: #8d6e63 !important
}
.brown.lighten-1,
.brown.lighten-1--after:after {
background-color: #8d6e63 !important
}
.brown--text.text--lighten-1 {
color: #8d6e63 !important
}
.brown--text.text--lighten-1 input,
.brown--text.text--lighten-1 textarea {
caret-color: #8d6e63 !important
}
.brown.darken-1 {
border-color: #6d4c41 !important
}
.brown.darken-1,
.brown.darken-1--after:after {
background-color: #6d4c41 !important
}
.brown--text.text--darken-1 {
color: #6d4c41 !important
}
.brown--text.text--darken-1 input,
.brown--text.text--darken-1 textarea {
caret-color: #6d4c41 !important
}
.brown.darken-2 {
border-color: #5d4037 !important
}
.brown.darken-2,
.brown.darken-2--after:after {
background-color: #5d4037 !important
}
.brown--text.text--darken-2 {
color: #5d4037 !important
}
.brown--text.text--darken-2 input,
.brown--text.text--darken-2 textarea {
caret-color: #5d4037 !important
}
.brown.darken-3 {
border-color: #4e342e !important
}
.brown.darken-3,
.brown.darken-3--after:after {
background-color: #4e342e !important
}
.brown--text.text--darken-3 {
color: #4e342e !important
}
.brown--text.text--darken-3 input,
.brown--text.text--darken-3 textarea {
caret-color: #4e342e !important
}
.brown.darken-4 {
border-color: #3e2723 !important
}
.brown.darken-4,
.brown.darken-4--after:after {
background-color: #3e2723 !important
}
.brown--text.text--darken-4 {
color: #3e2723 !important
}
.brown--text.text--darken-4 input,
.brown--text.text--darken-4 textarea {
caret-color: #3e2723 !important
}
.blue-grey {
background-color: #607d8b !important;
border-color: #607d8b !important
}
.blue-grey--text {
color: #607d8b !important
}
.blue-grey--text input,
.blue-grey--text textarea {
caret-color: #607d8b !important
}
.blue-grey--after:after {
background: #607d8b !important
}
.blue-grey.lighten-5 {
border-color: #eceff1 !important
}
.blue-grey.lighten-5,
.blue-grey.lighten-5--after:after {
background-color: #eceff1 !important
}
.blue-grey--text.text--lighten-5 {
color: #eceff1 !important
}
.blue-grey--text.text--lighten-5 input,
.blue-grey--text.text--lighten-5 textarea {
caret-color: #eceff1 !important
}
.blue-grey.lighten-4 {
border-color: #cfd8dc !important
}
.blue-grey.lighten-4,
.blue-grey.lighten-4--after:after {
background-color: #cfd8dc !important
}
.blue-grey--text.text--lighten-4 {
color: #cfd8dc !important
}
.blue-grey--text.text--lighten-4 input,
.blue-grey--text.text--lighten-4 textarea {
caret-color: #cfd8dc !important
}
.blue-grey.lighten-3 {
border-color: #b0bec5 !important
}
.blue-grey.lighten-3,
.blue-grey.lighten-3--after:after {
background-color: #b0bec5 !important
}
.blue-grey--text.text--lighten-3 {
color: #b0bec5 !important
}
.blue-grey--text.text--lighten-3 input,
.blue-grey--text.text--lighten-3 textarea {
caret-color: #b0bec5 !important
}
.blue-grey.lighten-2 {
border-color: #90a4ae !important
}
.blue-grey.lighten-2,
.blue-grey.lighten-2--after:after {
background-color: #90a4ae !important
}
.blue-grey--text.text--lighten-2 {
color: #90a4ae !important
}
.blue-grey--text.text--lighten-2 input,
.blue-grey--text.text--lighten-2 textarea {
caret-color: #90a4ae !important
}
.blue-grey.lighten-1 {
border-color: #78909c !important
}
.blue-grey.lighten-1,
.blue-grey.lighten-1--after:after {
background-color: #78909c !important
}
.blue-grey--text.text--lighten-1 {
color: #78909c !important
}
.blue-grey--text.text--lighten-1 input,
.blue-grey--text.text--lighten-1 textarea {
caret-color: #78909c !important
}
.blue-grey.darken-1 {
border-color: #546e7a !important
}
.blue-grey.darken-1,
.blue-grey.darken-1--after:after {
background-color: #546e7a !important
}
.blue-grey--text.text--darken-1 {
color: #546e7a !important
}
.blue-grey--text.text--darken-1 input,
.blue-grey--text.text--darken-1 textarea {
caret-color: #546e7a !important
}
.blue-grey.darken-2 {
border-color: #455a64 !important
}
.blue-grey.darken-2,
.blue-grey.darken-2--after:after {
background-color: #455a64 !important
}
.blue-grey--text.text--darken-2 {
color: #455a64 !important
}
.blue-grey--text.text--darken-2 input,
.blue-grey--text.text--darken-2 textarea {
caret-color: #455a64 !important
}
.blue-grey.darken-3 {
border-color: #37474f !important
}
.blue-grey.darken-3,
.blue-grey.darken-3--after:after {
background-color: #37474f !important
}
.blue-grey--text.text--darken-3 {
color: #37474f !important
}
.blue-grey--text.text--darken-3 input,
.blue-grey--text.text--darken-3 textarea {
caret-color: #37474f !important
}
.blue-grey.darken-4 {
border-color: #263238 !important
}
.blue-grey.darken-4,
.blue-grey.darken-4--after:after {
background-color: #263238 !important
}
.blue-grey--text.text--darken-4 {
color: #263238 !important
}
.blue-grey--text.text--darken-4 input,
.blue-grey--text.text--darken-4 textarea {
caret-color: #263238 !important
}
.grey {
background-color: #9e9e9e !important;
border-color: #9e9e9e !important
}
.grey--text {
color: #9e9e9e !important
}
.grey--text input,
.grey--text textarea {
caret-color: #9e9e9e !important
}
.grey--after:after {
background: #9e9e9e !important
}
.grey.lighten-5 {
border-color: #fafafa !important
}
.grey.lighten-5,
.grey.lighten-5--after:after {
background-color: #fafafa !important
}
.grey--text.text--lighten-5 {
color: #fafafa !important
}
.grey--text.text--lighten-5 input,
.grey--text.text--lighten-5 textarea {
caret-color: #fafafa !important
}
.grey.lighten-4 {
border-color: #f5f5f5 !important
}
.grey.lighten-4,
.grey.lighten-4--after:after {
background-color: #f5f5f5 !important
}
.grey--text.text--lighten-4 {
color: #f5f5f5 !important
}
.grey--text.text--lighten-4 input,
.grey--text.text--lighten-4 textarea {
caret-color: #f5f5f5 !important
}
.grey.lighten-3 {
border-color: #eee !important
}
.grey.lighten-3,
.grey.lighten-3--after:after {
background-color: #eee !important
}
.grey--text.text--lighten-3 {
color: #eee !important
}
.grey--text.text--lighten-3 input,
.grey--text.text--lighten-3 textarea {
caret-color: #eee !important
}
.grey.lighten-2 {
border-color: #e0e0e0 !important
}
.grey.lighten-2,
.grey.lighten-2--after:after {
background-color: #e0e0e0 !important
}
.grey--text.text--lighten-2 {
color: #e0e0e0 !important
}
.grey--text.text--lighten-2 input,
.grey--text.text--lighten-2 textarea {
caret-color: #e0e0e0 !important
}
.grey.lighten-1 {
border-color: #bdbdbd !important
}
.grey.lighten-1,
.grey.lighten-1--after:after {
background-color: #bdbdbd !important
}
.grey--text.text--lighten-1 {
color: #bdbdbd !important
}
.grey--text.text--lighten-1 input,
.grey--text.text--lighten-1 textarea {
caret-color: #bdbdbd !important
}
.grey.darken-1 {
border-color: #757575 !important
}
.grey.darken-1,
.grey.darken-1--after:after {
background-color: #757575 !important
}
.grey--text.text--darken-1 {
color: #757575 !important
}
.grey--text.text--darken-1 input,
.grey--text.text--darken-1 textarea {
caret-color: #757575 !important
}
.grey.darken-2 {
border-color: #616161 !important
}
.grey.darken-2,
.grey.darken-2--after:after {
background-color: #616161 !important
}
.grey--text.text--darken-2 {
color: #616161 !important
}
.grey--text.text--darken-2 input,
.grey--text.text--darken-2 textarea {
caret-color: #616161 !important
}
.grey.darken-3 {
border-color: #424242 !important
}
.grey.darken-3,
.grey.darken-3--after:after {
background-color: #424242 !important
}
.grey--text.text--darken-3 {
color: #424242 !important
}
.grey--text.text--darken-3 input,
.grey--text.text--darken-3 textarea {
caret-color: #424242 !important
}
.grey.darken-4 {
border-color: #212121 !important
}
.grey.darken-4,
.grey.darken-4--after:after {
background-color: #212121 !important
}
.grey--text.text--darken-4 {
color: #212121 !important
}
.grey--text.text--darken-4 input,
.grey--text.text--darken-4 textarea {
caret-color: #212121 !important
}
.shades.black {
border-color: #000 !important
}
.shades.black,
.shades.black--after:after {
background-color: #000 !important
}
.shades--text.text--black {
color: #000 !important
}
.shades--text.text--black input,
.shades--text.text--black textarea {
caret-color: #000 !important
}
.shades.white {
border-color: #fff !important
}
.shades.white,
.shades.white--after:after {
background-color: #fff !important
}
.shades--text.text--white {
color: #fff !important
}
.shades--text.text--white input,
.shades--text.text--white textarea {
caret-color: #fff !important
}
.shades.transparent {
border-color: transparent !important
}
.shades.transparent,
.shades.transparent--after:after {
background-color: transparent !important
}
.shades--text.text--transparent {
color: transparent !important
}
.shades--text.text--transparent input,
.shades--text.text--transparent textarea {
caret-color: transparent !important
}
.elevation-0 {
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12) !important
}
.elevation-1 {
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12) !important
}
.elevation-2 {
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12) !important
}
.elevation-3 {
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, .2), 0 3px 4px 0 rgba(0, 0, 0, .14), 0 1px 8px 0 rgba(0, 0, 0, .12) !important
}
.elevation-4 {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, .2), 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12) !important
}
.elevation-5 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 5px 8px 0 rgba(0, 0, 0, .14), 0 1px 14px 0 rgba(0, 0, 0, .12) !important
}
.elevation-6 {
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12) !important
}
.elevation-7 {
box-shadow: 0 4px 5px -2px rgba(0, 0, 0, .2), 0 7px 10px 1px rgba(0, 0, 0, .14), 0 2px 16px 1px rgba(0, 0, 0, .12) !important
}
.elevation-8 {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12) !important
}
.elevation-9 {
box-shadow: 0 5px 6px -3px rgba(0, 0, 0, .2), 0 9px 12px 1px rgba(0, 0, 0, .14), 0 3px 16px 2px rgba(0, 0, 0, .12) !important
}
.elevation-10 {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, .2), 0 10px 14px 1px rgba(0, 0, 0, .14), 0 4px 18px 3px rgba(0, 0, 0, .12) !important
}
.elevation-11 {
box-shadow: 0 6px 7px -4px rgba(0, 0, 0, .2), 0 11px 15px 1px rgba(0, 0, 0, .14), 0 4px 20px 3px rgba(0, 0, 0, .12) !important
}
.elevation-12 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, .2), 0 12px 17px 2px rgba(0, 0, 0, .14), 0 5px 22px 4px rgba(0, 0, 0, .12) !important
}
.elevation-13 {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, .2), 0 13px 19px 2px rgba(0, 0, 0, .14), 0 5px 24px 4px rgba(0, 0, 0, .12) !important
}
.elevation-14 {
box-shadow: 0 7px 9px -4px rgba(0, 0, 0, .2), 0 14px 21px 2px rgba(0, 0, 0, .14), 0 5px 26px 4px rgba(0, 0, 0, .12) !important
}
.elevation-15 {
box-shadow: 0 8px 9px -5px rgba(0, 0, 0, .2), 0 15px 22px 2px rgba(0, 0, 0, .14), 0 6px 28px 5px rgba(0, 0, 0, .12) !important
}
.elevation-16 {
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, .2), 0 16px 24px 2px rgba(0, 0, 0, .14), 0 6px 30px 5px rgba(0, 0, 0, .12) !important
}
.elevation-17 {
box-shadow: 0 8px 11px -5px rgba(0, 0, 0, .2), 0 17px 26px 2px rgba(0, 0, 0, .14), 0 6px 32px 5px rgba(0, 0, 0, .12) !important
}
.elevation-18 {
box-shadow: 0 9px 11px -5px rgba(0, 0, 0, .2), 0 18px 28px 2px rgba(0, 0, 0, .14), 0 7px 34px 6px rgba(0, 0, 0, .12) !important
}
.elevation-19 {
box-shadow: 0 9px 12px -6px rgba(0, 0, 0, .2), 0 19px 29px 2px rgba(0, 0, 0, .14), 0 7px 36px 6px rgba(0, 0, 0, .12) !important
}
.elevation-20 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, .2), 0 20px 31px 3px rgba(0, 0, 0, .14), 0 8px 38px 7px rgba(0, 0, 0, .12) !important
}
.elevation-21 {
box-shadow: 0 10px 13px -6px rgba(0, 0, 0, .2), 0 21px 33px 3px rgba(0, 0, 0, .14), 0 8px 40px 7px rgba(0, 0, 0, .12) !important
}
.elevation-22 {
box-shadow: 0 10px 14px -6px rgba(0, 0, 0, .2), 0 22px 35px 3px rgba(0, 0, 0, .14), 0 8px 42px 7px rgba(0, 0, 0, .12) !important
}
.elevation-23 {
box-shadow: 0 11px 14px -7px rgba(0, 0, 0, .2), 0 23px 36px 3px rgba(0, 0, 0, .14), 0 9px 44px 8px rgba(0, 0, 0, .12) !important
}
.elevation-24 {
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, .2), 0 24px 38px 3px rgba(0, 0, 0, .14), 0 9px 46px 8px rgba(0, 0, 0, .12) !important
}
html {
box-sizing: border-box;
overflow-y: scroll;
-webkit-text-size-adjust: 100%
}
*,
:after,
:before {
box-sizing: inherit
}
:after,
:before {
text-decoration: inherit;
vertical-align: inherit
}
* {
background-repeat: no-repeat;
padding: 0;
margin: 0
}
audio:not([controls]) {
display: none;
height: 0
}
hr {
overflow: visible
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
main,
menu,
nav,
section,
summary {
display: block
}
summary {
display: list-item
}
small {
font-size: 80%
}
[hidden],
template {
display: none
}
abbr[title] {
border-bottom: 1px dotted;
text-decoration: none
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects
}
a:active,
a:hover {
outline-width: 0
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace
}
b,
strong {
font-weight: bolder
}
dfn {
font-style: italic
}
mark {
background-color: #ff0;
color: #000
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sub {
bottom: -.25em
}
sup {
top: -.5em
}
input {
border-radius: 0
}
[role=button],
[type=button],
[type=reset],
[type=submit],
button {
cursor: pointer
}
[disabled] {
cursor: default
}
[type=number] {
width: auto
}
[type=search]::-webkit-search-cancel-button,
[type=search]::-webkit-search-decoration {
-webkit-appearance: none
}
textarea {
overflow: auto;
resize: vertical
}
button,
input,
optgroup,
select,
textarea {
font: inherit
}
optgroup {
font-weight: 700
}
button {
overflow: visible
}
[type=button]::-moz-focus-inner,
[type=reset]::-moz-focus-inner,
[type=submit]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: 0;
padding: 0
}
[type=button]::-moz-focus-inner,
[type=reset]::-moz-focus-inner,
[type=submit]::-moz-focus-inner,
button:-moz-focusring {
outline: 0;
border: 0
}
[type=reset],
[type=submit],
button,
html [type=button] {
-webkit-appearance: button
}
button,
select {
text-transform: none
}
button,
input,
select,
textarea {
background-color: transparent;
border-style: none;
color: inherit
}
select {
-moz-appearance: none;
-webkit-appearance: none
}
select::-ms-expand {
display: none
}
select::-ms-value {
color: currentColor
}
legend {
border: 0;
color: inherit;
display: table;
max-width: 100%;
white-space: normal
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px
}
img {
border-style: none
}
progress {
vertical-align: baseline
}
svg:not(:root) {
overflow: hidden
}
audio,
canvas,
progress,
video {
display: inline-block
}
[aria-busy=true] {
cursor: progress
}
[aria-controls] {
cursor: pointer
}
[aria-disabled] {
cursor: default
}
::-moz-selection {
background-color: #b3d4fc;
color: #000;
text-shadow: none
}
::selection {
background-color: #b3d4fc;
color: #000;
text-shadow: none
}
.bottom-sheet-transition-enter,
.bottom-sheet-transition-leave-to {
-webkit-transform: translateY(100%);
transform: translateY(100%)
}
.carousel-transition-enter {
-webkit-transform: translate(100%);
transform: translate(100%)
}
.carousel-transition-leave,
.carousel-transition-leave-to {
position: absolute;
top: 0
}
.carousel-reverse-transition-enter,
.carousel-transition-leave,
.carousel-transition-leave-to {
-webkit-transform: translate(-100%);
transform: translate(-100%)
}
.carousel-reverse-transition-leave,
.carousel-reverse-transition-leave-to {
position: absolute;
top: 0;
-webkit-transform: translate(100%);
transform: translate(100%)
}
.dialog-transition-enter,
.dialog-transition-leave-to {
-webkit-transform: scale(.5);
transform: scale(.5);
opacity: 0
}
.dialog-transition-enter-to,
.dialog-transition-leave {
opacity: 1
}
.dialog-bottom-transition-enter,
.dialog-bottom-transition-leave-to {
-webkit-transform: translateY(100%);
transform: translateY(100%)
}
.picker-reverse-transition-enter-active,
.picker-reverse-transition-leave-active,
.picker-transition-enter-active,
.picker-transition-leave-active {
transition: .3s cubic-bezier(0, 0, .2, 1)
}
.picker-reverse-transition-enter,
.picker-reverse-transition-leave-to,
.picker-transition-enter,
.picker-transition-leave-to {
opacity: 0
}
.picker-reverse-transition-enter-to,
.picker-transition-enter-to {
transtion: translate(0, 0)
}
.picker-reverse-transition-leave,
.picker-reverse-transition-leave-active,
.picker-reverse-transition-leave-to,
.picker-transition-leave,
.picker-transition-leave-active,
.picker-transition-leave-to {
position: absolute !important
}
.picker-transition-enter {
-webkit-transform: translateY(100%);
transform: translateY(100%)
}
.picker-reverse-transition-enter,
.picker-transition-leave-to {
-webkit-transform: translateY(-100%);
transform: translateY(-100%)
}
.picker-reverse-transition-leave-to {
-webkit-transform: translateY(100%);
transform: translateY(100%)
}
.picker-title-transition-enter-to,
.picker-title-transition-leave {
-webkit-transform: translate(0);
transform: translate(0)
}
.picker-title-transition-enter {
-webkit-transform: translate(-100%);
transform: translate(-100%)
}
.picker-title-transition-leave-to {
opacity: 0;
-webkit-transform: translate(100%);
transform: translate(100%)
}
.picker-title-transition-leave,
.picker-title-transition-leave-active,
.picker-title-transition-leave-to {
position: absolute !important
}
.tab-transition-enter {
-webkit-transform: translate(100%);
transform: translate(100%)
}
.tab-transition-enter-to {
-webkit-transform: translate(0);
transform: translate(0)
}
.tab-transition-leave,
.tab-transition-leave-active {
position: absolute;
top: 0
}
.tab-transition-leave-to {
position: absolute
}
.tab-reverse-transition-enter,
.tab-transition-leave-to {
-webkit-transform: translate(-100%);
transform: translate(-100%)
}
.tab-reverse-transition-leave,
.tab-reverse-transition-leave-to {
top: 0;
position: absolute;
-webkit-transform: translate(100%);
transform: translate(100%)
}
.scale-transition-enter-active,
.scale-transition-leave-active {
transition: .2s cubic-bezier(.4, 0, .6, 1)
}
.scale-transition-enter,
.scale-transition-leave,
.scale-transition-leave-to {
opacity: 0;
-webkit-transform: scale(0);
transform: scale(0)
}
.slide-y-transition-enter-active,
.slide-y-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.slide-y-transition-enter,
.slide-y-transition-leave-to {
opacity: 0;
-webkit-transform: translateY(-15px);
transform: translateY(-15px)
}
.slide-y-reverse-transition-enter-active,
.slide-y-reverse-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.slide-y-reverse-transition-enter,
.slide-y-reverse-transition-leave-to {
opacity: 0;
-webkit-transform: translateY(15px);
transform: translateY(15px)
}
.slide-x-transition-enter-active,
.slide-x-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.slide-x-transition-enter,
.slide-x-transition-leave-to {
opacity: 0;
-webkit-transform: translateX(-15px);
transform: translateX(-15px)
}
.slide-x-reverse-transition-enter-active,
.slide-x-reverse-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.slide-x-reverse-transition-enter,
.slide-x-reverse-transition-leave-to {
opacity: 0;
-webkit-transform: translateX(15px);
transform: translateX(15px)
}
.fade-transition-enter-active,
.fade-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.fade-transition-enter,
.fade-transition-leave-to {
opacity: 0
}
.fab-transition-enter-active,
.fab-transition-leave-active {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.fab-transition-enter,
.fab-transition-leave-to {
-webkit-transform: scale(0) rotate(-45deg);
transform: scale(0) rotate(-45deg)
}
.blockquote {
padding: 16px 0 16px 24px;
font-size: 18px;
font-weight: 300
}
code,
kbd {
display: inline-block;
border-radius: 3px;
white-space: pre-wrap;
font-size: 85%;
font-weight: 900
}
code:after,
code:before,
kbd:after,
kbd:before {
content: "\A0";
letter-spacing: -1px
}
code {
background-color: #f5f5f5;
color: #bd4147;
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12)
}
kbd {
background: #424242;
color: #fff
}
html {
font-size: 14px;
overflow-x: hidden;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0)
}
.application {
font-family: Roboto, sans-serif;
line-height: 1.5
}
::-ms-clear,
::-ms-reveal {
display: none
}
.browser-list {
padding-left: 24px
}
.browser-list--unstyled {
list-style-type: none
}
.display-4 {
font-size: 112px !important;
font-weight: 300;
line-height: 1 !important;
letter-spacing: -.04em !important
}
.display-3 {
font-size: 56px !important;
font-weight: 400;
line-height: 1.35 !important;
letter-spacing: -.02em !important
}
.display-2 {
font-size: 45px !important;
line-height: 48px !important
}
.display-1,
.display-2 {
font-weight: 400;
letter-spacing: normal !important
}
.display-1 {
font-size: 34px !important;
line-height: 40px !important
}
.headline {
font-size: 24px !important;
font-weight: 400;
line-height: 32px !important;
letter-spacing: normal !important
}
.title {
font-size: 20px !important;
font-weight: 500;
line-height: 1 !important;
letter-spacing: .02em !important
}
.subheading {
font-size: 16px !important;
font-weight: 400
}
.body-2 {
font-weight: 500
}
.body-1,
.body-2 {
font-size: 14px !important
}
.body-1,
.caption {
font-weight: 400
}
.caption {
font-size: 12px !important
}
p {
margin-bottom: 16px
}
.overflow-hidden {
overflow: hidden
}
.overflow-x-hidden {
overflow-x: hidden
}
.overflow-y-hidden {
overflow-y: hidden
}
.right {
float: right !important
}
.left {
float: left !important
}
.mx-auto {
margin-left: auto !important;
margin-right: auto !important
}
.mt-0 {
margin-top: 0 !important
}
.mr-0 {
margin-right: 0 !important
}
.mb-0 {
margin-bottom: 0 !important
}
.ml-0,
.mx-0 {
margin-left: 0 !important
}
.mx-0 {
margin-right: 0 !important
}
.my-0 {
margin-top: 0 !important;
margin-bottom: 0 !important
}
.ma-0 {
margin: 0 !important
}
.pt-0 {
padding-top: 0 !important
}
.pr-0 {
padding-right: 0 !important
}
.pb-0 {
padding-bottom: 0 !important
}
.pl-0,
.px-0 {
padding-left: 0 !important
}
.px-0 {
padding-right: 0 !important
}
.py-0 {
padding-top: 0 !important;
padding-bottom: 0 !important
}
.pa-0 {
padding: 0 !important
}
.mt-1 {
margin-top: 4px !important
}
.mr-1 {
margin-right: 4px !important
}
.mb-1 {
margin-bottom: 4px !important
}
.ml-1,
.mx-1 {
margin-left: 4px !important
}
.mx-1 {
margin-right: 4px !important
}
.my-1 {
margin-top: 4px !important;
margin-bottom: 4px !important
}
.ma-1 {
margin: 4px !important
}
.pt-1 {
padding-top: 4px !important
}
.pr-1 {
padding-right: 4px !important
}
.pb-1 {
padding-bottom: 4px !important
}
.pl-1,
.px-1 {
padding-left: 4px !important
}
.px-1 {
padding-right: 4px !important
}
.py-1 {
padding-top: 4px !important;
padding-bottom: 4px !important
}
.pa-1 {
padding: 4px !important
}
.mt-2 {
margin-top: 8px !important
}
.mr-2 {
margin-right: 8px !important
}
.mb-2 {
margin-bottom: 8px !important
}
.ml-2,
.mx-2 {
margin-left: 8px !important
}
.mx-2 {
margin-right: 8px !important
}
.my-2 {
margin-top: 8px !important;
margin-bottom: 8px !important
}
.ma-2 {
margin: 8px !important
}
.pt-2 {
padding-top: 8px !important
}
.pr-2 {
padding-right: 8px !important
}
.pb-2 {
padding-bottom: 8px !important
}
.pl-2,
.px-2 {
padding-left: 8px !important
}
.px-2 {
padding-right: 8px !important
}
.py-2 {
padding-top: 8px !important;
padding-bottom: 8px !important
}
.pa-2 {
padding: 8px !important
}
.mt-3 {
margin-top: 16px !important
}
.mr-3 {
margin-right: 16px !important
}
.mb-3 {
margin-bottom: 16px !important
}
.ml-3,
.mx-3 {
margin-left: 16px !important
}
.mx-3 {
margin-right: 16px !important
}
.my-3 {
margin-top: 16px !important;
margin-bottom: 16px !important
}
.ma-3 {
margin: 16px !important
}
.pt-3 {
padding-top: 16px !important
}
.pr-3 {
padding-right: 16px !important
}
.pb-3 {
padding-bottom: 16px !important
}
.pl-3,
.px-3 {
padding-left: 16px !important
}
.px-3 {
padding-right: 16px !important
}
.py-3 {
padding-top: 16px !important;
padding-bottom: 16px !important
}
.pa-3 {
padding: 16px !important
}
.mt-4 {
margin-top: 24px !important
}
.mr-4 {
margin-right: 24px !important
}
.mb-4 {
margin-bottom: 24px !important
}
.ml-4,
.mx-4 {
margin-left: 24px !important
}
.mx-4 {
margin-right: 24px !important
}
.my-4 {
margin-top: 24px !important;
margin-bottom: 24px !important
}
.ma-4 {
margin: 24px !important
}
.pt-4 {
padding-top: 24px !important
}
.pr-4 {
padding-right: 24px !important
}
.pb-4 {
padding-bottom: 24px !important
}
.pl-4,
.px-4 {
padding-left: 24px !important
}
.px-4 {
padding-right: 24px !important
}
.py-4 {
padding-top: 24px !important;
padding-bottom: 24px !important
}
.pa-4 {
padding: 24px !important
}
.mt-5 {
margin-top: 48px !important
}
.mr-5 {
margin-right: 48px !important
}
.mb-5 {
margin-bottom: 48px !important
}
.ml-5,
.mx-5 {
margin-left: 48px !important
}
.mx-5 {
margin-right: 48px !important
}
.my-5 {
margin-top: 48px !important;
margin-bottom: 48px !important
}
.ma-5 {
margin: 48px !important
}
.pt-5 {
padding-top: 48px !important
}
.pr-5 {
padding-right: 48px !important
}
.pb-5 {
padding-bottom: 48px !important
}
.pl-5,
.px-5 {
padding-left: 48px !important
}
.px-5 {
padding-right: 48px !important
}
.py-5 {
padding-top: 48px !important;
padding-bottom: 48px !important
}
.pa-5 {
padding: 48px !important
}
@media screen {
[hidden~=screen] {
display: inherit
}
[hidden~=screen]:not(:active):not(:focus):not(:target) {
position: absolute !important;
clip: rect(0 0 0 0) !important
}
}
@media only screen and (max-width:599px) {
.hidden-xs-only {
display: none !important
}
}
@media only screen and (min-width:600px) and (max-width:959px) {
.hidden-sm-only {
display: none !important
}
}
@media only screen and (max-width:959px) {
.hidden-sm-and-down {
display: none !important
}
}
@media only screen and (min-width:600px) {
.hidden-sm-and-up {
display: none !important
}
}
@media only screen and (min-width:960px) and (max-width:1263px) {
.hidden-md-only {
display: none !important
}
}
@media only screen and (max-width:1263px) {
.hidden-md-and-down {
display: none !important
}
}
@media only screen and (min-width:960px) {
.hidden-md-and-up {
display: none !important
}
}
@media only screen and (min-width:1264px) and (max-width:1903px) {
.hidden-lg-only {
display: none !important
}
}
@media only screen and (max-width:1903px) {
.hidden-lg-and-down {
display: none !important
}
}
@media only screen and (min-width:1264px) {
.hidden-lg-and-up {
display: none !important
}
}
@media only screen and (min-width:1904px) {
.hidden-xl-only {
display: none !important
}
}
@media (min-width:0) {
.text-xs-left {
text-align: left !important
}
.text-xs-center {
text-align: center !important
}
.text-xs-right {
text-align: right !important
}
.text-xs-justify {
text-align: justify !important
}
}
@media (min-width:600px) {
.text-sm-left {
text-align: left !important
}
.text-sm-center {
text-align: center !important
}
.text-sm-right {
text-align: right !important
}
.text-sm-justify {
text-align: justify !important
}
}
@media (min-width:960px) {
.text-md-left {
text-align: left !important
}
.text-md-center {
text-align: center !important
}
.text-md-right {
text-align: right !important
}
.text-md-justify {
text-align: justify !important
}
}
@media (min-width:1264px) {
.text-lg-left {
text-align: left !important
}
.text-lg-center {
text-align: center !important
}
.text-lg-right {
text-align: right !important
}
.text-lg-justify {
text-align: justify !important
}
}
@media (min-width:1904px) {
.text-xl-left {
text-align: left !important
}
.text-xl-center {
text-align: center !important
}
.text-xl-right {
text-align: right !important
}
.text-xl-justify {
text-align: justify !important
}
}
.application,
.application--wrap {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.application--wrap {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
min-height: 100vh;
max-width: 100%;
position: relative
}
.application.theme--light {
background: #fafafa;
color: rgba(0, 0, 0, .87)
}
.application.theme--light a {
cursor: pointer
}
.application.theme--light .text--primary {
color: rgba(0, 0, 0, .87) !important
}
.application.theme--light .text--secondary {
color: rgba(0, 0, 0, .54) !important
}
.application.theme--light .text--disabled {
color: rgba(0, 0, 0, .38) !important
}
.application.theme--dark {
background: #303030;
color: #fff
}
.application.theme--dark a {
cursor: pointer
}
.application.theme--dark .text--primary {
color: #fff !important
}
.application.theme--dark .text--secondary {
color: hsla(0, 0%, 100%, .7) !important
}
.application.theme--dark .text--disabled {
color: hsla(0, 0%, 100%, .5) !important
}
@-moz-document url-prefix() {
@media print {
.application,
.application--wrap {
display: block
}
}
}
.alert {
border-radius: 0;
border-width: 4px 0 0;
border-style: solid;
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 14px;
margin: 4px auto;
padding: 16px;
position: relative;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.alert .alert__icon.icon,
.alert__dismissible .icon {
-ms-flex-item-align: center;
align-self: center;
color: rgba(0, 0, 0, .3);
font-size: 24px
}
.alert--outline .icon {
color: inherit !important
}
.alert__icon {
margin-right: 16px
}
.alert__dismissible {
-ms-flex-item-align: start;
align-self: flex-start;
color: inherit;
margin-left: 16px;
margin-right: 0;
text-decoration: none;
transition: .3s cubic-bezier(.25, .8, .5, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.alert__dismissible:hover {
opacity: .8
}
.alert--no-icon .alert__icon {
display: none
}
.alert>div {
-ms-flex-item-align: center;
align-self: center;
-webkit-box-flex: 1;
-ms-flex: 1 1;
flex: 1 1
}
.alert.alert {
border-color: rgba(0, 0, 0, .12) !important
}
.alert.alert--outline {
border: 1px solid currentColor !important
}
@media screen and (max-width:600px) {
.alert__icon {
display: none
}
}
.application .theme--light.icon,
.theme--light .icon {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.icon.icon--disabled:not(.input-group__append-icon),
.theme--light .icon.icon--disabled:not(.input-group__append-icon) {
color: rgba(0, 0, 0, .38) !important
}
.application .theme--dark.icon,
.theme--dark .icon {
color: #fff
}
.application .theme--dark.icon.icon--disabled:not(.input-group__append-icon),
.theme--dark .icon.icon--disabled:not(.input-group__append-icon) {
color: hsla(0, 0%, 100%, .5) !important
}
.icon {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-font-feature-settings: "liga";
font-feature-settings: "liga";
font-size: 24px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
line-height: 1;
transition: .3s cubic-bezier(.25, .8, .5, 1);
vertical-align: middle
}
.icon.icon--large {
font-size: 2.5rem
}
.icon.icon--medium {
font-size: 2rem
}
.icon.icon--x-large {
font-size: 3rem
}
.icon.icon--disabled {
pointer-events: none
}
.avatar {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: 50%;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: relative;
text-align: center;
vertical-align: middle
}
.avatar .icon,
.avatar img {
border-radius: 50%;
height: inherit;
width: inherit
}
.avatar--tile,
.avatar--tile .icon,
.avatar--tile img {
border-radius: 0
}
.badge {
display: inline-block;
position: relative
}
.badge__badge {
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: absolute;
top: -11px;
right: -22px;
border-radius: 50%;
height: 22px;
width: 22px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.badge__badge,
.badge__badge .icon {
font-size: 14px
}
.badge--overlap .badge__badge {
top: -8px;
right: -8px
}
.badge--overlap.badge--left .badge__badge {
left: -8px;
right: auto
}
.badge--overlap.badge--bottom .badge__badge {
bottom: -8px;
top: auto
}
.badge--left .badge__badge {
left: -22px
}
.badge--bottom .badge__badge {
bottom: -11px;
top: auto
}
.application .theme--light.bottom-nav,
.theme--light .bottom-nav {
background-color: #fff
}
.application .theme--dark.bottom-nav,
.theme--dark .bottom-nav {
background-color: #424242
}
.bottom-nav {
bottom: 0;
box-shadow: 0 3px 14px 2px rgba(0, 0, 0, .12);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-transform: translateY(60px);
transform: translateY(60px);
transition: all .4s cubic-bezier(.25, .8, .5, 1);
width: 100%;
z-index: 4
}
.bottom-nav--absolute {
position: absolute
}
.bottom-nav--active {
-webkit-transform: translate(0);
transform: translate(0)
}
.bottom-nav--fixed {
position: fixed
}
.bottom-nav .btn {
background: transparent !important;
border-radius: 0;
box-shadow: none !important;
font-weight: 400;
height: 100%;
margin: 0;
max-width: 168px;
min-width: 80px;
padding: 6px 0 10px;
text-transform: none;
opacity: .5;
width: 100%
}
.bottom-nav .btn .btn__content {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
font-size: 12px;
white-space: nowrap;
will-change: font-size
}
.bottom-nav .btn .btn__content i.icon {
color: inherit;
margin-bottom: 4px;
transition: all .4s cubic-bezier(.25, .8, .5, 1)
}
.bottom-nav .btn .btn__content span {
line-height: 1
}
.bottom-nav .btn--active {
opacity: 1
}
.bottom-nav .btn--active .btn__content {
font-size: 14px
}
.bottom-nav .btn--active .btn__content:before {
opacity: 0
}
.bottom-nav .btn--active .btn__content .icon {
-webkit-transform: none;
transform: none
}
.bottom-nav .btn:not(.btn--active) {
-webkit-filter: grayscale(100%);
filter: grayscale(100%)
}
.bottom-nav--shift .btn__content {
font-size: 14px
}
.bottom-nav--shift .btn {
transition: all .3s;
min-width: 56px;
max-width: 96px
}
.bottom-nav--shift .btn--active {
min-width: 96px;
max-width: 168px
}
.bottom-nav--shift .btn:not(.btn--active) .btn__content .icon {
-webkit-transform: scale(1) translateY(10px);
transform: scale(1) translateY(10px)
}
.bottom-nav--shift .btn:not(.btn--active) .btn__content span {
color: transparent
}
.bottom-sheet.dialog {
-ms-flex-item-align: end;
align-self: flex-end;
border-radius: 0;
-webkit-box-flex: 1;
-ms-flex: 1 0 100%;
flex: 1 0 100%;
margin: 0;
min-width: 100%;
overflow: visible;
transition: .4s cubic-bezier(.25, .8, .5, 1)
}
.bottom-sheet.dialog.bottom-sheet--inset {
max-width: 70%;
min-width: 0
}
@media only screen and (max-width:599px) {
.bottom-sheet.dialog.bottom-sheet--inset {
max-width: none
}
}
.dialog {
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, .2), 0 24px 38px 3px rgba(0, 0, 0, .14), 0 9px 46px 8px rgba(0, 0, 0, .12);
border-radius: 2px;
margin: 24px;
overflow-y: auto;
pointer-events: auto
}
.dialog,
.dialog__content {
transition: .3s ease-in-out;
width: 100%
}
.dialog__content {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
left: 0;
pointer-events: none;
position: fixed;
top: 0;
z-index: 6;
outline: none
}
.dialog:not(.dialog--fullscreen) {
max-height: 90%
}
.dialog__container {
display: inline-block;
vertical-align: middle
}
.dialog--fullscreen {
margin: 0;
height: 100%;
position: fixed;
overflow-y: auto;
top: 0;
left: 0
}
.dialog--fullscreen>.card {
min-height: 100%;
min-width: 100%;
margin: 0 !important;
padding: 0 !important
}
.dialog--scrollable,
.dialog--scrollable>.card {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.dialog--scrollable>.card {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column
}
.dialog--scrollable>.card>.card__actions,
.dialog--scrollable>.card>.card__title {
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto
}
.dialog--scrollable>.card>.card__text {
overflow-y: auto;
-webkit-backface-visibility: hidden;
backface-visibility: hidden
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
transition: .5s cubic-bezier(.25, .8, .5, 1);
z-index: 5
}
.overlay--absolute,
.overlay:before {
position: absolute
}
.overlay:before {
background-color: #212121;
bottom: 0;
content: "";
height: 100%;
left: 0;
opacity: 0;
right: 0;
top: 0;
transition: inherit;
transition-delay: .15s;
width: 100%
}
.overlay--active {
pointer-events: auto;
-ms-touch-action: none;
touch-action: none
}
.overlay--active:before {
opacity: .46
}
.application .theme--light.breadcrumbs li.breadcrumbs__divider,
.application .theme--light.breadcrumbs li .breadcrumbs__item--disabled,
.application .theme--light.breadcrumbs li:last-child .breadcrumbs__item,
.theme--light .breadcrumbs li.breadcrumbs__divider,
.theme--light .breadcrumbs li .breadcrumbs__item--disabled,
.theme--light .breadcrumbs li:last-child .breadcrumbs__item {
color: rgba(0, 0, 0, .38)
}
.application .theme--dark.breadcrumbs li.breadcrumbs__divider,
.application .theme--dark.breadcrumbs li .breadcrumbs__item--disabled,
.application .theme--dark.breadcrumbs li:last-child .breadcrumbs__item,
.theme--dark .breadcrumbs li.breadcrumbs__divider,
.theme--dark .breadcrumbs li .breadcrumbs__item--disabled,
.theme--dark .breadcrumbs li:last-child .breadcrumbs__item {
color: hsla(0, 0%, 100%, .5)
}
.breadcrumbs {
-ms-flex-align: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
list-style-type: none;
margin: 0;
padding: 18px 12px
}
.breadcrumbs,
.breadcrumbs li {
-webkit-box-align: center;
align-items: center
}
.breadcrumbs li {
-ms-flex-align: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 14px
}
.breadcrumbs li .icon {
font-size: 16px
}
.breadcrumbs li:last-child a {
cursor: default;
pointer-events: none
}
.breadcrumbs li:nth-child(2n) {
padding: 0 12px
}
.breadcrumbs--large li,
.breadcrumbs--large li .icon {
font-size: 16px
}
.breadcrumbs__item {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
text-decoration: none;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.breadcrumbs__item--disabled {
pointer-events: none
}
.ripple__container {
border-radius: inherit;
width: 100%;
height: 100%;
z-index: 0;
contain: strict
}
.ripple__animation,
.ripple__container {
color: inherit;
position: absolute;
left: 0;
top: 0;
overflow: hidden;
pointer-events: none
}
.ripple__animation {
border-radius: 50%;
background: currentColor;
opacity: 0;
transition: .4s cubic-bezier(0, 0, .2, 1);
will-change: transform, opacity
}
.ripple__animation--enter {
transition: none
}
.ripple__animation--visible {
opacity: .15
}
.application .theme--light.btn,
.theme--light .btn {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.btn.btn--disabled,
.application .theme--light.btn.btn--disabled .icon,
.theme--light .btn.btn--disabled,
.theme--light .btn.btn--disabled .icon {
color: rgba(0, 0, 0, .26) !important
}
.application .theme--light.btn.btn--disabled:not(.btn--icon):not(.btn--flat),
.theme--light .btn.btn--disabled:not(.btn--icon):not(.btn--flat) {
background-color: rgba(0, 0, 0, .12) !important
}
.application .theme--light.btn:not(.btn--icon):not(.btn--flat),
.theme--light .btn:not(.btn--icon):not(.btn--flat) {
background-color: #f5f5f5
}
.application .theme--dark.btn,
.theme--dark .btn {
color: #fff
}
.application .theme--dark.btn.btn--disabled,
.application .theme--dark.btn.btn--disabled .icon,
.theme--dark .btn.btn--disabled,
.theme--dark .btn.btn--disabled .icon {
color: hsla(0, 0%, 100%, .3) !important
}
.application .theme--dark.btn.btn--disabled:not(.btn--icon):not(.btn--flat),
.theme--dark .btn.btn--disabled:not(.btn--icon):not(.btn--flat) {
background-color: hsla(0, 0%, 100%, .12) !important
}
.application .theme--dark.btn:not(.btn--icon):not(.btn--flat),
.theme--dark .btn:not(.btn--icon):not(.btn--flat) {
background-color: #212121
}
.btn {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: 2px;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
height: 36px;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
font-size: 14px;
font-weight: 500;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
margin: 6px 8px;
min-width: 88px;
outline: 0;
text-transform: uppercase;
text-decoration: none;
transition: .3s cubic-bezier(.25, .8, .5, 1), color 1ms;
position: relative;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.btn__content:before {
border-radius: inherit;
color: inherit;
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
opacity: .12;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%
}
.btn--small {
font-size: 13px;
height: 28px
}
.btn--small .btn__content {
padding: 0 8px
}
.btn--large {
font-size: 15px;
height: 44px
}
.btn--large .btn__content {
padding: 0 32px
}
.btn--active .btn__content:before,
.btn:focus .btn__content:before,
.btn:hover .btn__content:before {
background-color: currentColor
}
.btn__content {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: inherit;
color: inherit;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: inherit;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
margin: 0 auto;
padding: 0 16px;
transition: .3s cubic-bezier(.25, .8, .5, 1);
white-space: nowrap;
width: inherit
}
.btn .btn__content .icon {
color: inherit
}
.btn--flat {
background-color: transparent !important;
box-shadow: none !important
}
.btn:not(.btn--depressed) {
will-change: box-shadow;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.btn:not(.btn--depressed):active {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12)
}
.btn:not(.btn--depressed):active .btn__content,
.btn:not(.btn--depressed):focus .btn__content {
position: relative;
top: 0;
left: 0
}
.btn--icon {
background: transparent;
box-shadow: none !important;
border-radius: 50%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
min-width: 0;
width: 36px
}
.btn--icon.btn--small {
width: 28px
}
.btn--icon.btn--large {
width: 44px
}
.btn--floating,
.btn--icon .btn__content:before {
border-radius: 50%
}
.btn--floating {
min-width: 0;
height: 56px;
width: 56px;
padding: 0;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12)
}
.btn--floating.btn--absolute,
.btn--floating.btn--fixed {
z-index: 4
}
.btn--floating:active {
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, .2), 0 12px 17px 2px rgba(0, 0, 0, .14), 0 5px 22px 4px rgba(0, 0, 0, .12)
}
.btn--floating .btn__content {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin: 0;
padding: 0
}
.btn--floating:after {
border-radius: 50%
}
.btn--floating .btn__content :not(:only-child) {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.btn--floating .btn__content :not(:only-child):first-child {
opacity: 1
}
.btn--floating .btn__content :not(:only-child):last-child {
opacity: 0;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg)
}
.btn--floating .btn__content :not(:only-child):first-child,
.btn--floating .btn__content :not(:only-child):last-child {
-webkit-backface-visibility: hidden;
position: absolute;
left: 0;
top: 0
}
.btn--floating.btn--active .btn__content :not(:only-child):first-child {
opacity: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg)
}
.btn--floating.btn--active .btn__content :not(:only-child):last-child {
opacity: 1;
-webkit-transform: rotate(0);
transform: rotate(0)
}
.btn--floating .icon {
height: inherit;
width: inherit
}
.btn--floating.btn--small {
height: 40px;
width: 40px
}
.btn--floating.btn--small .icon {
font-size: 18px
}
.btn--floating.btn--large {
height: 72px;
width: 72px
}
.btn--floating.btn--large .icon {
font-size: 30px
}
.btn--reverse .btn__content {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse
}
.btn--reverse.btn--column .btn__content {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse
}
.btn--absolute,
.btn--fixed {
margin: 0
}
.btn.btn--absolute {
position: absolute
}
.btn.btn--fixed {
position: fixed
}
.btn--top:not(.btn--absolute) {
top: 16px
}
.btn--top.btn--absolute {
top: -28px
}
.btn--top.btn--absolute.btn--small {
top: -20px
}
.btn--top.btn--absolute.btn--large {
top: -36px
}
.btn--bottom:not(.btn--absolute) {
bottom: 16px
}
.btn--bottom.btn--absolute {
bottom: -28px
}
.btn--bottom.btn--absolute.btn--small {
bottom: -20px
}
.btn--bottom.btn--absolute.btn--large {
bottom: -36px
}
.btn--left {
left: 16px
}
.btn--right {
right: 16px
}
.btn.btn--disabled {
box-shadow: none !important;
pointer-events: none
}
.btn--icon .btn__content {
padding: 0
}
.btn--loader {
pointer-events: none
}
.btn--loader .btn__content {
opacity: 0
}
.btn__loading {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100%
}
.btn__loading .icon--left {
margin-right: 1rem;
line-height: inherit
}
.btn__loading .icon--right {
margin-left: 1rem;
line-height: inherit
}
.btn.btn--outline {
border: 1px solid currentColor;
background: transparent !important;
box-shadow: none
}
.btn.btn--outline:hover {
box-shadow: none
}
.btn--block {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
margin: 6px 0;
width: 100%
}
.btn--round,
.btn--round:after {
border-radius: 28px
}
.btn .icon--right {
margin-left: 16px
}
.btn .icon--left {
margin-right: 16px
}
.btn.accent,
.btn.error,
.btn.info,
.btn.primary,
.btn.secondary,
.btn.success,
.btn.warning {
color: #fff
}
.application .theme--light.btn-toggle,
.theme--light .btn-toggle {
background: #fff
}
.application .theme--light.btn-toggle .btn,
.theme--light .btn-toggle .btn {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.btn-toggle .btn.btn--active:not(:last-child):not([data-only-child]),
.theme--light .btn-toggle .btn.btn--active:not(:last-child):not([data-only-child]) {
border-right-color: rgba(0, 0, 0, .26)
}
.application .theme--dark.btn-toggle,
.theme--dark .btn-toggle {
background: #424242
}
.application .theme--dark.btn-toggle .btn,
.theme--dark .btn-toggle .btn {
color: #fff
}
.application .theme--dark.btn-toggle .btn.btn--active:not(:last-child):not([data-only-child]),
.theme--dark .btn-toggle .btn.btn--active:not(:last-child):not([data-only-child]) {
border-right-color: hsla(0, 0%, 100%, .3)
}
.btn-toggle {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
border-radius: 2px;
transition: .3s cubic-bezier(.25, .8, .5, 1);
will-change: background, box-shadow
}
.btn-toggle .btn {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
min-width: auto;
width: auto;
padding: 0 8px;
margin: 0;
opacity: .4;
border-radius: 0
}
.btn-toggle .btn:not(:last-child) {
border-right: 1px solid transparent
}
.btn-toggle .btn:after {
display: none
}
.btn-toggle .btn.btn--active {
opacity: 1
}
.btn-toggle .btn__content {
padding: 0
}
.btn-toggle .btn span+.icon {
font-size: medium;
margin-left: 10px
}
.btn-toggle .btn:first-child {
border-radius: 2px 0 0 2px
}
.btn-toggle .btn:last-child {
border-radius: 0 2px 2px 0
}
.btn-toggle--selected {
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.application .theme--light.card,
.theme--light .card {
background-color: #fff;
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.card,
.theme--dark .card {
background-color: #424242;
color: #fff
}
.card {
display: block;
border-radius: 2px;
min-width: 0;
position: relative;
text-decoration: none;
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12)
}
.card>:first-child:not(.btn) {
border-top-left-radius: inherit;
border-top-right-radius: inherit
}
.card>:last-child:not(.btn) {
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit
}
.card--raised {
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, .2), 0 3px 4px 0 rgba(0, 0, 0, .14), 0 1px 8px 0 rgba(0, 0, 0, .12)
}
.card--tile {
border-radius: 0
}
.card--flat {
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12)
}
.card--hover {
cursor: pointer;
transition: all .4s cubic-bezier(.25, .8, .25, 1);
transition-property: box-shadow
}
.card--hover:hover {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12)
}
.card__title {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
padding: 16px
}
.card__title--primary {
padding-top: 24px
}
.card__text {
padding: 16px;
width: 100%
}
.card__media {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
overflow: hidden;
position: relative
}
.card__media img {
width: 100%
}
.card__media__background {
border-radius: inherit;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%
}
.card__media__content {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
position: relative
}
.card__actions,
.card__media__content {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.card__actions {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 8px 4px
}
.card__actions .btn,
.card__actions>* {
margin: 0 4px
}
.carousel {
height: 500px;
width: 100%;
position: relative;
overflow: hidden;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.carousel__left,
.carousel__right {
position: absolute;
top: 50%;
z-index: 1;
-webkit-transform: translateY(-50%);
transform: translateY(-50%)
}
.carousel__left .btn,
.carousel__right .btn {
color: #fff;
margin: 0 !important;
height: auto;
width: auto
}
.carousel__left .btn i,
.carousel__right .btn i {
font-size: 48px
}
.carousel__left .btn:hover,
.carousel__right .btn:hover {
background: none
}
.carousel__left {
left: 5px
}
.carousel__right {
right: 5px
}
.carousel__controls {
background: rgba(0, 0, 0, .5);
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
bottom: 0;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
left: 0;
position: absolute;
height: 50px;
list-style-type: none;
width: 100%;
z-index: 1
}
.carousel__controls__item {
color: #fff;
margin: 0 8px !important
}
.carousel__controls__item i {
opacity: .5;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.carousel__controls__item--active i {
opacity: 1;
vertical-align: middle
}
.carousel__controls__item:hover {
background: none
}
.carousel__controls__item:hover i {
opacity: .8
}
.application .theme--light.jumbotron__content,
.theme--light .jumbotron__content {
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.jumbotron__content,
.theme--dark .jumbotron__content {
color: #fff
}
.jumbotron {
display: block;
top: 0;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%
}
.jumbotron__wrapper {
height: 100%;
overflow: hidden;
position: relative;
transition: inherit;
width: 100%
}
.jumbotron__background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
contain: strict;
transition: inherit
}
.jumbotron__image {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
min-width: 100%;
will-change: transform;
transition: inherit
}
.jumbotron__content {
height: 100%;
position: relative;
transition: inherit
}
.application .theme--light.input-group input,
.application .theme--light.input-group textarea,
.theme--light .input-group input,
.theme--light .input-group textarea {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.input-group input:disabled,
.application .theme--light.input-group textarea:disabled,
.theme--light .input-group input:disabled,
.theme--light .input-group textarea:disabled {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group:not(.input-group--error) .input-group__messages,
.theme--light .input-group:not(.input-group--error) .input-group__messages {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group.input-group--textarea:not(.input-group--full-width) .input-group__input,
.theme--light .input-group.input-group--textarea:not(.input-group--full-width) .input-group__input {
border: 2px solid rgba(0, 0, 0, .54)
}
.application .theme--light.input-group.input-group--solo,
.theme--light .input-group.input-group--solo {
background: #fff
}
.application .theme--light.input-group.input-group--solo-inverted,
.theme--light .input-group.input-group--solo-inverted {
background: rgba(0, 0, 0, .16)
}
.application .theme--light.input-group.input-group--solo-inverted.input-group--focused,
.theme--light .input-group.input-group--solo-inverted.input-group--focused {
background: #424242
}
.application .theme--light.input-group.input-group--solo-inverted.input-group--focused .input-group__append-icon,
.application .theme--light.input-group.input-group--solo-inverted.input-group--focused .input-group__prepend-icon,
.application .theme--light.input-group.input-group--solo-inverted.input-group--focused input,
.application .theme--light.input-group.input-group--solo-inverted.input-group--focused label,
.theme--light .input-group.input-group--solo-inverted.input-group--focused .input-group__append-icon,
.theme--light .input-group.input-group--solo-inverted.input-group--focused .input-group__prepend-icon,
.theme--light .input-group.input-group--solo-inverted.input-group--focused input,
.theme--light .input-group.input-group--solo-inverted.input-group--focused label {
color: #fff
}
.application .theme--light.input-group.input-group--dirty .input-group__selections__comma:not(.input-group__selections__comma--active),
.theme--light .input-group.input-group--dirty .input-group__selections__comma:not(.input-group__selections__comma--active) {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.input-group:not(.input-group--error) label,
.theme--light .input-group:not(.input-group--error) label {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group:not(.input-group--error).input-group--disabled .input-group__selections__comma,
.application .theme--light.input-group:not(.input-group--error).input-group--disabled label,
.theme--light .input-group:not(.input-group--error).input-group--disabled .input-group__selections__comma,
.theme--light .input-group:not(.input-group--error).input-group--disabled label {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group:not(.input-group--error) .input-group__details:before,
.theme--light .input-group:not(.input-group--error) .input-group__details:before {
background-color: rgba(0, 0, 0, .42)
}
.application .theme--light.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__append-icon:not(:hover),
.application .theme--light.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__prepend-icon:not(:hover),
.theme--light .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__append-icon:not(:hover),
.theme--light .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__prepend-icon:not(:hover) {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover .input-group__details:before,
.theme--light .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover .input-group__details:before {
background-color: rgba(0, 0, 0, .87)
}
.application .theme--light.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover.input-group--textarea:not(.input-group--full-width) .input-group__input,
.theme--light .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover.input-group--textarea:not(.input-group--full-width) .input-group__input {
border-color: rgba(0, 0, 0, .87)
}
.application .theme--light.input-group.input-group--editable .input-group__details:before,
.application .theme--light.input-group.input-group--editable .input-group__input:before,
.application .theme--light.input-group.input-group--overflow .input-group__details:before,
.application .theme--light.input-group.input-group--overflow .input-group__input:before,
.application .theme--light.input-group.input-group--segmented .input-group__details:before,
.application .theme--light.input-group.input-group--segmented .input-group__input:before,
.theme--light .input-group.input-group--editable .input-group__details:before,
.theme--light .input-group.input-group--editable .input-group__input:before,
.theme--light .input-group.input-group--overflow .input-group__details:before,
.theme--light .input-group.input-group--overflow .input-group__input:before,
.theme--light .input-group.input-group--segmented .input-group__details:before,
.theme--light .input-group.input-group--segmented .input-group__input:before {
background-color: rgba(0, 0, 0, .12)
}
.application .theme--light.input-group.input-group--disabled .input-group__input .input-group__append-icon,
.application .theme--light.input-group.input-group--disabled .input-group__input .input-group__prepend-icon,
.theme--light .input-group.input-group--disabled .input-group__input .input-group__append-icon,
.theme--light .input-group.input-group--disabled .input-group__input .input-group__prepend-icon {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group.input-group--disabled .input-group__details:before,
.theme--light .input-group.input-group--disabled .input-group__details:before {
background-color: transparent;
background-image: linear-gradient(90deg, rgba(0, 0, 0, .38) 0, rgba(0, 0, 0, .38) 33%, transparent 0)
}
.application .theme--light.input-group .input-group--text-field__prefix,
.application .theme--light.input-group .input-group--text-field__suffix,
.theme--light .input-group .input-group--text-field__prefix,
.theme--light .input-group .input-group--text-field__suffix {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group .input-group--text-field.input-group--disabled__prefix,
.application .theme--light.input-group .input-group--text-field.input-group--disabled__suffix,
.theme--light .input-group .input-group--text-field.input-group--disabled__prefix,
.theme--light .input-group .input-group--text-field.input-group--disabled__suffix {
color: rgba(0, 0, 0, .38)
}
.application .theme--dark.input-group input,
.application .theme--dark.input-group textarea,
.theme--dark .input-group input,
.theme--dark .input-group textarea {
color: #fff
}
.application .theme--dark.input-group input:disabled,
.application .theme--dark.input-group textarea:disabled,
.theme--dark .input-group input:disabled,
.theme--dark .input-group textarea:disabled {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group:not(.input-group--error) .input-group__messages,
.theme--dark .input-group:not(.input-group--error) .input-group__messages {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group.input-group--textarea:not(.input-group--full-width) .input-group__input,
.theme--dark .input-group.input-group--textarea:not(.input-group--full-width) .input-group__input {
border: 2px solid hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group.input-group--solo,
.theme--dark .input-group.input-group--solo {
background: #424242
}
.application .theme--dark.input-group.input-group--solo-inverted,
.theme--dark .input-group.input-group--solo-inverted {
background: hsla(0, 0%, 100%, .16)
}
.application .theme--dark.input-group.input-group--solo-inverted.input-group--focused,
.theme--dark .input-group.input-group--solo-inverted.input-group--focused {
background: #fff
}
.application .theme--dark.input-group.input-group--solo-inverted.input-group--focused .input-group__append-icon,
.application .theme--dark.input-group.input-group--solo-inverted.input-group--focused .input-group__prepend-icon,
.application .theme--dark.input-group.input-group--solo-inverted.input-group--focused input,
.application .theme--dark.input-group.input-group--solo-inverted.input-group--focused label,
.theme--dark .input-group.input-group--solo-inverted.input-group--focused .input-group__append-icon,
.theme--dark .input-group.input-group--solo-inverted.input-group--focused .input-group__prepend-icon,
.theme--dark .input-group.input-group--solo-inverted.input-group--focused input,
.theme--dark .input-group.input-group--solo-inverted.input-group--focused label {
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.input-group.input-group--dirty .input-group__selections__comma:not(.input-group__selections__comma--active),
.theme--dark .input-group.input-group--dirty .input-group__selections__comma:not(.input-group__selections__comma--active) {
color: #fff
}
.application .theme--dark.input-group:not(.input-group--error) label,
.theme--dark .input-group:not(.input-group--error) label {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group:not(.input-group--error).input-group--disabled .input-group__selections__comma,
.application .theme--dark.input-group:not(.input-group--error).input-group--disabled label,
.theme--dark .input-group:not(.input-group--error).input-group--disabled .input-group__selections__comma,
.theme--dark .input-group:not(.input-group--error).input-group--disabled label {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group:not(.input-group--error) .input-group__details:before,
.theme--dark .input-group:not(.input-group--error) .input-group__details:before {
background-color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__append-icon:not(:hover),
.application .theme--dark.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__prepend-icon:not(:hover),
.theme--dark .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__append-icon:not(:hover),
.theme--dark .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled) .input-group__input .input-group__prepend-icon:not(:hover) {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover .input-group__details:before,
.theme--dark .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover .input-group__details:before {
background-color: #fff
}
.application .theme--dark.input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover.input-group--textarea:not(.input-group--full-width) .input-group__input,
.theme--dark .input-group:not(.input-group--error):not(.input-group--focused):not(.input-group--disabled):not(.input-group--disabled):not(.input-group--overflow):not(.input-group--segmented):not(.input-group--editable):hover.input-group--textarea:not(.input-group--full-width) .input-group__input {
border-color: #fff
}
.application .theme--dark.input-group.input-group--editable .input-group__details:before,
.application .theme--dark.input-group.input-group--editable .input-group__input:before,
.application .theme--dark.input-group.input-group--overflow .input-group__details:before,
.application .theme--dark.input-group.input-group--overflow .input-group__input:before,
.application .theme--dark.input-group.input-group--segmented .input-group__details:before,
.application .theme--dark.input-group.input-group--segmented .input-group__input:before,
.theme--dark .input-group.input-group--editable .input-group__details:before,
.theme--dark .input-group.input-group--editable .input-group__input:before,
.theme--dark .input-group.input-group--overflow .input-group__details:before,
.theme--dark .input-group.input-group--overflow .input-group__input:before,
.theme--dark .input-group.input-group--segmented .input-group__details:before,
.theme--dark .input-group.input-group--segmented .input-group__input:before {
background-color: hsla(0, 0%, 100%, .12)
}
.application .theme--dark.input-group.input-group--disabled .input-group__input .input-group__append-icon,
.application .theme--dark.input-group.input-group--disabled .input-group__input .input-group__prepend-icon,
.theme--dark .input-group.input-group--disabled .input-group__input .input-group__append-icon,
.theme--dark .input-group.input-group--disabled .input-group__input .input-group__prepend-icon {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group.input-group--disabled .input-group__details:before,
.theme--dark .input-group.input-group--disabled .input-group__details:before {
background-color: transparent;
background-image: linear-gradient(90deg, hsla(0, 0%, 100%, .5) 0, hsla(0, 0%, 100%, .5) 33%, transparent 0)
}
.application .theme--dark.input-group .input-group--text-field__prefix,
.application .theme--dark.input-group .input-group--text-field__suffix,
.theme--dark .input-group .input-group--text-field__prefix,
.theme--dark .input-group .input-group--text-field__suffix {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group .input-group--text-field.input-group--disabled__prefix,
.application .theme--dark.input-group .input-group--text-field.input-group--disabled__suffix,
.theme--dark .input-group .input-group--text-field.input-group--disabled__prefix,
.theme--dark .input-group .input-group--text-field.input-group--disabled__suffix {
color: hsla(0, 0%, 100%, .5)
}
.input-group {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1 1;
flex: 1 1;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
min-width: 24px;
padding: 18px 0 0;
position: relative;
width: 100%;
outline: none;
transition: box-shadow .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group input {
width: 100%
}
.input-group label {
display: inline-block;
font-size: 16px;
line-height: 30px;
height: 30px;
max-width: 90%;
min-width: 0;
overflow: hidden;
pointer-events: none;
text-align: left;
text-overflow: ellipsis;
-webkit-transform-origin: top left;
transform-origin: top left;
transition: .4s cubic-bezier(.25, .8, .25, 1);
white-space: nowrap;
width: 100%;
z-index: 0
}
.input-group__input {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1 0 100%;
flex: 1 0 100%;
min-width: 0;
min-height: 30px
}
.input-group__icon-cb {
cursor: pointer
}
.input-group.input-group--error .input-group__append-icon,
.input-group.input-group--error .input-group__prepend-icon,
.input-group.input-group--focused .input-group__append-icon,
.input-group.input-group--focused .input-group__prepend-icon {
color: inherit
}
.input-group.input-group--append-icon label,
.input-group.input-group--prepend-icon label,
.input-group.input-group--selection-controls label {
max-width: 75%
}
.input-group.input-group--append-icon.input-group--prepend-icon label {
max-width: 65%
}
.input-group .input-group__append-icon {
padding: 0 6px
}
.input-group .input-group__append-icon,
.input-group .input-group__prepend-icon {
-ms-flex-item-align: center;
align-self: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.input-group.input-group--single-line.input-group--dirty label,
.input-group.input-group--solo.input-group--dirty label {
display: none
}
.input-group.input-group--solo {
min-height: 46px;
border-radius: 2px;
padding: 0;
transition: .3s cubic-bezier(.25, .8, .5, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.input-group.input-group--solo label {
top: 8px;
padding-left: 16px;
-webkit-transform: none !important;
transform: none !important
}
.input-group.input-group--solo .input-group__input {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 8px 16px
}
.input-group.input-group--solo .input-group__details {
display: none
}
.input-group--disabled {
pointer-events: none
}
.input-group--disabled .input-group__details:before {
background-color: transparent;
background-position: bottom;
background-size: 3px 1px;
background-repeat: repeat-x
}
.input-group.input-group--text-field:not(.input-group--single-line):not(.input-group--error).input-group--focused label {
color: inherit
}
.input-group.input-group--text-field:not(.input-group--single-line):not(.input-group--error).input-group--focused .input-group__input {
border-color: inherit
}
.input-group.input-group--text-field.input-group--focused:not(.input-group--disabled) .input-group__details:after {
-webkit-transform: scaleX(1);
transform: scaleX(1)
}
.input-group--required label:after {
content: "*"
}
.input-group.input-group--error label {
-webkit-animation: a .6s cubic-bezier(.25, .8, .5, 1);
animation: a .6s cubic-bezier(.25, .8, .5, 1)
}
.input-group.input-group--error .input-group__messages {
color: inherit
}
.input-group.input-group--error .input-group__details:before {
background-color: currentColor
}
.input-group .slide-y-transition-leave,
.input-group .slide-y-transition-leave-to {
position: absolute
}
.input-group__details {
color: inherit;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
padding-top: 4px;
-webkit-box-flex: 1;
-ms-flex: 1 0 100%;
flex: 1 0 100%;
font-size: 12px;
min-height: 26px;
overflow: hidden;
position: relative;
width: 100%
}
.input-group__details:after,
.input-group__details:before {
content: "";
position: absolute;
left: 0;
transition: .3s cubic-bezier(.4, 0, .2, 1)
}
.input-group__details:after {
background-color: currentColor;
color: inherit;
top: 0;
height: 2px;
-webkit-transform: scaleX(0);
transform: scaleX(0);
-webkit-transform-origin: center center 0;
transform-origin: center center 0;
width: 100%;
z-index: 1
}
.input-group__details:before {
top: 0;
height: 1px;
width: 100%;
z-index: 0
}
.input-group--hide-details .input-group__details {
min-height: 2px;
padding: 0
}
.input-group--async-loading .input-group__details:after,
.input-group--async-loading .input-group__details:before {
display: none
}
.input-group .progress-linear {
position: absolute;
top: 0;
left: 0;
margin: 0
}
.input-group__error,
.input-group__hint {
transition: .3s cubic-bezier(.25, .8, .25, 1)
}
.input-group__error {
color: inherit;
-webkit-box-flex: 1;
-ms-flex: 1 0 100%;
flex: 1 0 100%
}
.input-group--editable.input-group--active,
.input-group--overflow.input-group--active,
.input-group--segmented.input-group--active {
background-color: #fff
}
.application .theme--light.input-group--selection-controls label,
.theme--light .input-group--selection-controls label {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.input-group--selection-controls .icon--selection-control,
.theme--light .input-group--selection-controls .icon--selection-control {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group--selection-controls.input-group--active .icon--selection-control,
.theme--light .input-group--selection-controls.input-group--active .icon--selection-control {
color: inherit
}
.application .theme--light.input-group--selection-controls.input-group--disabled .input-group__input,
.theme--light .input-group--selection-controls.input-group--disabled .input-group__input {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group--selection-controls.input-group--disabled .input-group__input .icon--checkbox,
.application .theme--light.input-group--selection-controls.input-group--disabled .input-group__input .icon--radio,
.theme--light .input-group--selection-controls.input-group--disabled .input-group__input .icon--checkbox,
.theme--light .input-group--selection-controls.input-group--disabled .input-group__input .icon--radio {
color: inherit
}
.application .theme--dark.input-group--selection-controls label,
.theme--dark .input-group--selection-controls label {
color: #fff
}
.application .theme--dark.input-group--selection-controls .icon--selection-control,
.theme--dark .input-group--selection-controls .icon--selection-control {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group--selection-controls.input-group--active .icon--selection-control,
.theme--dark .input-group--selection-controls.input-group--active .icon--selection-control {
color: inherit
}
.application .theme--dark.input-group--selection-controls.input-group--disabled .input-group__input,
.theme--dark .input-group--selection-controls.input-group--disabled .input-group__input {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group--selection-controls.input-group--disabled .input-group__input .icon--checkbox,
.application .theme--dark.input-group--selection-controls.input-group--disabled .input-group__input .icon--radio,
.theme--dark .input-group--selection-controls.input-group--disabled .input-group__input .icon--checkbox,
.theme--dark .input-group--selection-controls.input-group--disabled .input-group__input .icon--radio {
color: inherit
}
.input-group--selection-controls.input-group--tab-focused .input-group--selection-controls__ripple:before,
.input-group--tab-focused .input-group:focus .input-group--selection-controls__ripple:before {
-webkit-transform: translate(-50%, -50%) scale(1);
transform: translate(-50%, -50%) scale(1);
opacity: .15
}
.input-group.input-group--selection-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
padding: 0
}
.input-group.input-group--selection-controls .icon--selection-control {
cursor: pointer;
position: absolute;
left: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
transition: .3s cubic-bezier(.4, 0, .6, 1)
}
.input-group.input-group--selection-controls .input-group__details:after,
.input-group.input-group--selection-controls .input-group__details:before {
display: none
}
.input-group.input-group--selection-controls .input-group__input {
color: inherit;
width: 100%;
position: relative
}
.input-group.input-group--selection-controls .input-group__input .icon--selection-control {
-ms-flex-item-align: center;
align-self: center;
height: 30px;
margin: auto
}
.input-group.input-group--selection-controls.input-group--error .input-group__input .icon--selection-control,
.input-group.input-group--selection-controls.input-group--error label {
color: inherit
}
.input-group.input-group--selection-controls label {
cursor: pointer;
position: absolute;
left: 32px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
z-index: 1
}
.input-group.input-group--selection-controls:not(.input-group--disabled) label {
pointer-events: all
}
.input-group--selection-controls__ripple {
border-radius: 50%;
height: 48px;
width: 48px;
cursor: pointer;
position: absolute;
-webkit-transform: translate(-12px, -50%);
transform: translate(-12px, -50%);
-webkit-transform-origin: center center;
transform-origin: center center;
top: 50%;
left: 0
}
.input-group--selection-controls__ripple:before {
content: "";
position: absolute;
width: 36px;
height: 36px;
background: currentColor;
border-radius: 50%;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%) scale(.3);
transform: translate(-50%, -50%) scale(.3);
opacity: 0;
transition: .4s cubic-bezier(0, 0, .2, 1);
-webkit-transform-origin: center center;
transform-origin: center center
}
.input-group--prepend-icon.input-group--selection-controls .icon--selection-control,
.input-group--prepend-icon.input-group--selection-controls .input-group--selection-controls__ripple {
left: 38px
}
.input-group--prepend-icon.input-group--selection-controls label {
left: 76px
}
.input-group--append-icon.input-group--selection-controls .input-group__append-icon {
position: absolute;
left: 32px
}
.input-group--append-icon.input-group--selection-controls.input-group--selection-controls label {
left: 76px
}
.input-group--append-icon.input-group--prepend-icon.input-group--selection-controls .input-group__append-icon {
left: 72px
}
.input-group--append-icon.input-group--prepend-icon.input-group--selection-controls.input-group--selection-controls label {
left: 112px
}
.input-group--prepend-icon.radio-group--row .icon--selection-control,
.input-group--prepend-icon.radio-group--row .input-group--selection-controls__ripple {
left: 14px
}
.input-group--prepend-icon.radio-group--row label {
left: 52px
}
.application .theme--light.chip,
.theme--light .chip {
background: #e0e0e0;
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.chip,
.theme--dark .chip {
background: #fff;
color: rgba(0, 0, 0, .87)
}
.chip {
border-radius: 28px;
border: 1px solid transparent;
font-size: 13px;
margin: 4px;
outline: none;
position: relative;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.chip,
.chip .chip__content {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
vertical-align: middle
}
.chip .chip__content {
border-radius: 28px;
cursor: default;
height: 32px;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 0 12px;
white-space: nowrap;
z-index: 1
}
.chip--removable .chip__content {
padding: 0 4px 0 12px
}
.chip .avatar {
height: 32px !important;
margin-left: -12px;
margin-right: 8px;
min-width: 32px;
width: 32px !important
}
.chip .avatar img {
height: 100%;
width: 100%
}
.chip--active:not(.chip--disabled),
.chip--selected:not(.chip--disabled),
.chip:focus:not(.chip--disabled) {
border-color: rgba(0, 0, 0, .13);
overflow: hidden;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.chip--active:not(.chip--disabled):after,
.chip--selected:not(.chip--disabled):after,
.chip:focus:not(.chip--disabled):after {
background: currentColor;
border-radius: inherit;
content: "";
height: 100%;
position: absolute;
top: 0;
left: 0;
transition: inherit;
width: 100%;
pointer-events: none;
opacity: .13
}
.chip--label,
.chip--label .chip__content {
border-radius: 2px
}
.chip.chip--outline {
background: transparent !important;
border-color: currentColor;
color: #9e9e9e
}
.chip--small {
height: 26px
}
.chip--small .avatar {
height: 26px;
min-width: 26px;
width: 26px
}
.chip--small .icon,
.chip__close {
font-size: 20px
}
.chip__close {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: inherit;
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
margin: 0 2px 0 8px;
text-decoration: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.chip__close>.icon {
color: inherit !important;
font-size: 20px;
opacity: .5
}
.chip__close>.icon:hover {
opacity: 1
}
.chip--select-multi {
margin: 4px 4px 4px 0
}
.chip .icon {
color: inherit
}
.chip .icon--right {
margin-left: 12px;
margin-right: -8px
}
.chip .icon--left {
margin-left: -8px;
margin-right: 12px
}
.application .theme--light.data-iterator .data-iterator__actions,
.theme--light .data-iterator .data-iterator__actions {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.data-iterator .data-iterator__actions__select .input-group--select .input-group__append-icon,
.application .theme--light.data-iterator .data-iterator__actions__select .input-group--select .input-group__selections__comma,
.theme--light .data-iterator .data-iterator__actions__select .input-group--select .input-group__append-icon,
.theme--light .data-iterator .data-iterator__actions__select .input-group--select .input-group__selections__comma {
color: rgba(0, 0, 0, .54) !important
}
.application .theme--dark.data-iterator .data-iterator__actions,
.theme--dark .data-iterator .data-iterator__actions {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.data-iterator .data-iterator__actions__select .input-group--select .input-group__append-icon,
.application .theme--dark.data-iterator .data-iterator__actions__select .input-group--select .input-group__selections__comma,
.theme--dark .data-iterator .data-iterator__actions__select .input-group--select .input-group__append-icon,
.theme--dark .data-iterator .data-iterator__actions__select .input-group--select .input-group__selections__comma {
color: hsla(0, 0%, 100%, .7) !important
}
.data-iterator__actions {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 12px;
-ms-flex-wrap: wrap-reverse;
flex-wrap: wrap-reverse
}
.data-iterator__actions .btn {
color: inherit
}
.data-iterator__actions .btn:last-of-type {
margin-left: 14px
}
.data-iterator__actions__range-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
min-height: 48px
}
.data-iterator__actions__pagination {
display: block;
text-align: center;
margin: 0 32px 0 24px
}
.data-iterator__actions__select {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
margin-right: 14px
}
.data-iterator__actions__select .input-group--select {
margin: 13px 0 13px 34px;
padding: 0;
position: static
}
.data-iterator__actions__select .input-group--select .input-group__selections__comma {
font-size: 12px
}
.application .theme--light.input-group--text-field.input-group--text-field-box .input-group__input,
.theme--light .input-group--text-field.input-group--text-field-box .input-group__input {
background: hsla(0, 0%, 100%, .6)
}
.application .theme--light.input-group--text-field input::-webkit-input-placeholder,
.application .theme--light.input-group--text-field textarea::-webkit-input-placeholder,
.theme--light .input-group--text-field input::-webkit-input-placeholder,
.theme--light .input-group--text-field textarea::-webkit-input-placeholder {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group--text-field input:-ms-input-placeholder,
.application .theme--light.input-group--text-field input::-ms-input-placeholder,
.application .theme--light.input-group--text-field textarea:-ms-input-placeholder,
.application .theme--light.input-group--text-field textarea::-ms-input-placeholder,
.theme--light .input-group--text-field input:-ms-input-placeholder,
.theme--light .input-group--text-field input::-ms-input-placeholder,
.theme--light .input-group--text-field textarea:-ms-input-placeholder,
.theme--light .input-group--text-field textarea::-ms-input-placeholder {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group--text-field input::placeholder,
.application .theme--light.input-group--text-field textarea::placeholder,
.theme--light .input-group--text-field input::placeholder,
.theme--light .input-group--text-field textarea::placeholder {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.input-group--text-field:not(.input-group--error) .input-group__counter,
.theme--light .input-group--text-field:not(.input-group--error) .input-group__counter {
color: rgba(0, 0, 0, .54)
}
.application .theme--dark.input-group--text-field.input-group--text-field-box .input-group__input,
.theme--dark .input-group--text-field.input-group--text-field-box .input-group__input {
background: hsla(0, 0%, 100%, .1)
}
.application .theme--dark.input-group--text-field input::-webkit-input-placeholder,
.application .theme--dark.input-group--text-field textarea::-webkit-input-placeholder,
.theme--dark .input-group--text-field input::-webkit-input-placeholder,
.theme--dark .input-group--text-field textarea::-webkit-input-placeholder {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group--text-field input:-ms-input-placeholder,
.application .theme--dark.input-group--text-field input::-ms-input-placeholder,
.application .theme--dark.input-group--text-field textarea:-ms-input-placeholder,
.application .theme--dark.input-group--text-field textarea::-ms-input-placeholder,
.theme--dark .input-group--text-field input:-ms-input-placeholder,
.theme--dark .input-group--text-field input::-ms-input-placeholder,
.theme--dark .input-group--text-field textarea:-ms-input-placeholder,
.theme--dark .input-group--text-field textarea::-ms-input-placeholder {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group--text-field input::placeholder,
.application .theme--dark.input-group--text-field textarea::placeholder,
.theme--dark .input-group--text-field input::placeholder,
.theme--dark .input-group--text-field textarea::placeholder {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.input-group--text-field:not(.input-group--error) .input-group__counter,
.theme--dark .input-group--text-field:not(.input-group--error) .input-group__counter {
color: hsla(0, 0%, 100%, .7)
}
.input-group--text-field label {
position: absolute;
top: 18px;
left: 0
}
.input-group--text-field input {
padding-bottom: 1px
}
.input-group--text-field input,
.input-group--text-field textarea {
font-size: 16px
}
.input-group--text-field input::-webkit-input-placeholder,
.input-group--text-field textarea::-webkit-input-placeholder {
color: inherit;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group--text-field input:-ms-input-placeholder,
.input-group--text-field input::-ms-input-placeholder,
.input-group--text-field textarea:-ms-input-placeholder,
.input-group--text-field textarea::-ms-input-placeholder {
color: inherit;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group--text-field input::placeholder,
.input-group--text-field textarea::placeholder {
color: inherit;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group--text-field input {
box-shadow: none;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
height: 30px;
margin: 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.input-group--text-field input:focus {
outline: none
}
.input-group--text-field input:disabled {
pointer-events: none
}
.input-group--text-field textarea {
-webkit-box-flex: 1;
-ms-flex: 1 1;
flex: 1 1
}
.input-group--text-field textarea:focus {
outline: none
}
.input-group--text-field.input-group--textarea label {
top: 13px
}
.input-group--text-field.input-group--textarea .input-group__input {
border-radius: 2px;
transition: .4s cubic-bezier(.25, .8, .25, 1)
}
.input-group--text-field.input-group--textarea textarea {
font-size: 16px;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group--text-field.input-group--textarea:not(.input-group--full-width) label {
top: 30px;
left: 15px
}
.input-group--text-field.input-group--textarea:not(.input-group--full-width) .input-group__input {
padding: 30px 0 0 13px
}
.input-group--text-field.input-group--textarea .input-group__details:after,
.input-group--text-field.input-group--textarea .input-group__details:before {
opacity: 0
}
.input-group--text-field .input-group__counter {
margin-left: auto
}
.input-group--text-field .input-group__counter--error {
color: inherit
}
.input-group--text-field.input-group--placeholder.input-group--dirty input::-webkit-input-placeholder,
.input-group--text-field.input-group--placeholder.input-group--dirty textarea::-webkit-input-placeholder {
opacity: 0
}
.input-group--text-field.input-group--placeholder.input-group--dirty input:-ms-input-placeholder,
.input-group--text-field.input-group--placeholder.input-group--dirty input::-ms-input-placeholder,
.input-group--text-field.input-group--placeholder.input-group--dirty textarea:-ms-input-placeholder,
.input-group--text-field.input-group--placeholder.input-group--dirty textarea::-ms-input-placeholder {
opacity: 0
}
.input-group--text-field.input-group--placeholder.input-group--dirty input::placeholder,
.input-group--text-field.input-group--placeholder.input-group--dirty textarea::placeholder {
opacity: 0
}
.input-group--text-field.input-group--no-resize textarea {
resize: none
}
.input-group--text-field.input-group--prepend-icon .input-group__prepend-icon {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
min-width: 40px
}
.input-group--text-field.input-group--prepend-icon .input-group__details {
margin-left: auto;
max-width: calc(100% - 40px)
}
.input-group--text-field.input-group--prepend-icon label {
left: 40px
}
.input-group--text-field:not(.input-group--single-line).input-group--focused label,
.input-group--text-field:not(.input-group--single-line).input-group--placeholder label {
opacity: 1
}
.input-group--text-field:not(.input-group--single-line).input-group--focused:not(.input-group--textarea) label,
.input-group--text-field:not(.input-group--single-line).input-group--placeholder:not(.input-group--textarea) label {
-webkit-transform: translateY(-18px) scale(.75);
transform: translateY(-18px) scale(.75)
}
.input-group--text-field:not(.input-group--single-line).input-group--focused:not(.input-group--full-width).input-group--textarea label,
.input-group--text-field:not(.input-group--single-line).input-group--placeholder:not(.input-group--full-width).input-group--textarea label {
-webkit-transform: translateY(-8px) scale(.75);
transform: translateY(-8px) scale(.75)
}
.input-group--text-field:not(.input-group--single-line).input-group--focused.input-group--text-field-box label,
.input-group--text-field:not(.input-group--single-line).input-group--placeholder.input-group--text-field-box label {
-webkit-transform: translateY(-10px) scale(.75);
transform: translateY(-10px) scale(.75)
}
.input-group--text-field.input-group--dirty.input-group--select label,
.input-group--text-field.input-group--dirty:not(.input-group--textarea) label {
-webkit-transform: translateY(-18px) scale(.75);
transform: translateY(-18px) scale(.75)
}
.input-group--text-field.input-group--dirty:not(.input-group--full-width).input-group--textarea label {
-webkit-transform: translateY(-8px) scale(.75);
transform: translateY(-8px) scale(.75)
}
.input-group--text-field.input-group--multi-line textarea {
padding-top: 4px
}
.input-group--text-field.input-group--full-width {
padding: 16px
}
.input-group--text-field.input-group--full-width label {
margin-left: 16px
}
.input-group--text-field.input-group--full-width .input-group__details:after,
.input-group--text-field.input-group--full-width .input-group__details:before {
display: none
}
.input-group--prefix:not(.input-group--focused):not(.input-group--dirty) label {
left: 16px
}
.input-group--prefix .input-group--text-field__prefix,
.input-group--prefix .input-group--text-field__suffix,
.input-group--suffix .input-group--text-field__prefix,
.input-group--suffix .input-group--text-field__suffix {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 16px;
margin-top: 1px
}
.input-group--prefix .input-group--text-field__prefix,
.input-group--suffix .input-group--text-field__prefix {
margin-right: 3px
}
.input-group--prefix .input-group--text-field__suffix,
.input-group--suffix .input-group--text-field__suffix {
margin-left: 3px
}
.input-group--text-field-box input,
.input-group--text-field-box textarea {
cursor: pointer
}
.input-group--text-field-box label {
left: 16px
}
.input-group--text-field-box .input-group__input {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
border-radius: 4px 4px 0 0;
cursor: pointer;
min-height: 56px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.input-group--text-field-box .input-group__details {
padding: 8px 16px 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.input-group--text-field-box .input-group__details:after,
.input-group--text-field-box .input-group__details:before {
height: 2px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px
}
.input-group--text-field-box.input-group--multi-line textarea {
padding-left: 24px;
padding-right: 24px
}
.input-group--text-field-box:not(.input-group--textarea).input-group--multi-line .input-group__input {
padding-top: 26px
}
.input-group--text-field-box:not(.input-group--textarea).input-group--multi-line label {
top: 26px
}
.input-group--text-field-box:not(.input-group--textarea):not(.input-group--multi-line) .input-group__input {
padding-left: 16px;
padding-right: 16px
}
.input-group--text-field-box:not(.input-group--textarea):not(.input-group--multi-line) label {
top: 32px
}
.input-group--text-field-box:not(.input-group--textarea):not(.input-group--single-line).input-group--dirty label,
.input-group--text-field-box:not(.input-group--textarea):not(.input-group--single-line).input-group--focused label {
-webkit-transform: translateY(-10px) scale(.75);
transform: translateY(-10px) scale(.75)
}
.input-group--text-field-box.input-group--prepend-icon .input-group__details:after,
.input-group--text-field-box.input-group--prepend-icon .input-group__details:before {
margin-left: 56px;
max-width: calc(100% - 56px)
}
.input-group--text-field-box.input-group--prepend-icon label {
left: 56px
}
.application .theme--light.input-group--select.input-group--editable.input-group--focused .input-group__input,
.application .theme--light.input-group--select.input-group--editable .input-group__input:hover,
.application .theme--light.input-group--select.input-group--overflow.input-group--focused .input-group__input,
.application .theme--light.input-group--select.input-group--overflow .input-group__input:hover,
.application .theme--light.input-group--select.input-group--segmented.input-group--focused .input-group__input,
.application .theme--light.input-group--select.input-group--segmented .input-group__input:hover,
.theme--light .input-group--select.input-group--editable.input-group--focused .input-group__input,
.theme--light .input-group--select.input-group--editable .input-group__input:hover,
.theme--light .input-group--select.input-group--overflow.input-group--focused .input-group__input,
.theme--light .input-group--select.input-group--overflow .input-group__input:hover,
.theme--light .input-group--select.input-group--segmented.input-group--focused .input-group__input,
.theme--light .input-group--select.input-group--segmented .input-group__input:hover {
background: #fff
}
.application .theme--dark.input-group--select.input-group--editable.input-group--focused .input-group__input,
.application .theme--dark.input-group--select.input-group--editable .input-group__input:hover,
.application .theme--dark.input-group--select.input-group--overflow.input-group--focused .input-group__input,
.application .theme--dark.input-group--select.input-group--overflow .input-group__input:hover,
.application .theme--dark.input-group--select.input-group--segmented.input-group--focused .input-group__input,
.application .theme--dark.input-group--select.input-group--segmented .input-group__input:hover,
.theme--dark .input-group--select.input-group--editable.input-group--focused .input-group__input,
.theme--dark .input-group--select.input-group--editable .input-group__input:hover,
.theme--dark .input-group--select.input-group--overflow.input-group--focused .input-group__input,
.theme--dark .input-group--select.input-group--overflow .input-group__input:hover,
.theme--dark .input-group--select.input-group--segmented.input-group--focused .input-group__input,
.theme--dark .input-group--select.input-group--segmented .input-group__input:hover {
background: #424242
}
.input-group--select .input-group--select__autocomplete {
display: block;
height: 0
}
.input-group--select .input-group--select__autocomplete--index {
background-color: transparent !important
}
.input-group--select .input-group__append-icon {
transition: .3s cubic-bezier(0, 0, .2, 1)
}
.input-group--select .input-group__append-icon.input-group__icon-clearable {
transition: none
}
.input-group--select.input-group--focused .input-group--select__autocomplete,
.input-group--select:not(.input-group--dirty) .input-group--select__autocomplete {
padding-bottom: 1px;
height: 30px
}
.input-group--select.input-group--focused .input-group--select__autocomplete {
display: inline-block;
opacity: 1
}
.input-group--select.input-group--focused.input-group--select--selecting-index .input-group--select__autocomplete {
opacity: 0
}
.input-group--select.input-group--focused.input-group--open .input-group__append-icon:not(.input-group__icon-clearable) {
-webkit-transform: rotate(-180deg);
transform: rotate(-180deg)
}
.input-group--select .input-group__input {
cursor: pointer
}
.input-group--select.input-group--disabled {
cursor: default;
pointer-events: none
}
.input-group--select .input-group__selections {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
position: relative;
width: 100%
}
.input-group--select .input-group__selections__comma {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 16px;
padding: 3px 4px 3px 0
}
.input-group--select .input-group__selections__comma--active {
color: inherit
}
.input-group--select .menu {
display: inline
}
.input-group--select .fade-transition-leave-active {
position: absolute;
left: 0
}
.input-group--select.input-group--autocomplete.input-group--search-focused .input-group__selections__comma {
display: none
}
.input-group--autocomplete .input-group__selections {
cursor: text
}
.input-group.input-group--chips .input-group__input {
padding-top: 0;
padding-bottom: 0
}
.input-group.input-group--editable,
.input-group.input-group--editable .input-group__append-icon,
.input-group.input-group--overflow,
.input-group.input-group--overflow .input-group__append-icon,
.input-group.input-group--segmented,
.input-group.input-group--segmented .input-group__append-icon {
padding: 0
}
.input-group.input-group--editable .input-group__selections,
.input-group.input-group--editable input,
.input-group.input-group--overflow .input-group__selections,
.input-group.input-group--overflow input,
.input-group.input-group--segmented .input-group__selections,
.input-group.input-group--segmented input {
height: 48px
}
.input-group.input-group--editable .input-group__selections__comma,
.input-group.input-group--editable input,
.input-group.input-group--overflow .input-group__selections__comma,
.input-group.input-group--overflow input,
.input-group.input-group--segmented .input-group__selections__comma,
.input-group.input-group--segmented input {
top: 0;
left: 0;
padding-left: 16px
}
.input-group.input-group--editable .input-group__selections,
.input-group.input-group--overflow .input-group__selections,
.input-group.input-group--segmented .input-group__selections {
width: calc(100% - 55px)
}
.input-group.input-group--editable .input-group__selections .btn,
.input-group.input-group--overflow .input-group__selections .btn,
.input-group.input-group--segmented .input-group__selections .btn {
border-radius: 0;
margin: 0;
height: 48px;
width: 100%
}
.input-group.input-group--editable .input-group__selections .btn .btn__content,
.input-group.input-group--overflow .input-group__selections .btn .btn__content,
.input-group.input-group--segmented .input-group__selections .btn .btn__content {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: start
}
.input-group.input-group--editable .input-group__input,
.input-group.input-group--overflow .input-group__input,
.input-group.input-group--segmented .input-group__input {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group.input-group--editable.input-group--focused .input-group__input,
.input-group.input-group--overflow.input-group--focused .input-group__input,
.input-group.input-group--segmented.input-group--focused .input-group__input {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12)
}
.input-group.input-group--editable label,
.input-group.input-group--overflow label,
.input-group.input-group--segmented label {
left: 16px !important;
top: 9px !important
}
.input-group.input-group--editable .input-group__details:after,
.input-group.input-group--overflow .input-group__details:after,
.input-group.input-group--segmented .input-group__details:after {
display: none
}
.input-group.input-group--editable .input-group__input,
.input-group.input-group--overflow .input-group__input,
.input-group.input-group--segmented .input-group__input {
padding: 0
}
.input-group.input-group--editable .input-group__input:before,
.input-group.input-group--overflow .input-group__input:before,
.input-group.input-group--segmented .input-group__input:before {
content: "";
position: absolute;
left: 0;
width: 100%;
height: 1px;
top: 0;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.input-group.input-group--editable .input-group__append-icon,
.input-group.input-group--overflow .input-group__append-icon,
.input-group.input-group--segmented .input-group__append-icon {
width: 55px
}
.input-group--editable .input-group__input:hover:after,
.input-group--segmented .input-group__input:after,
.input-group.input-group--focused.input-group--editable .input-group__input:after {
background-color: rgba(0, 0, 0, .12);
content: "";
position: absolute;
right: 55px;
height: 48px;
top: 0;
width: 1px
}
.menu__content--select .input-group--selection-controls__ripple {
display: none
}
.menu__content--autocomplete,
.menu__content--autocomplete>.card {
border-radius: 0
}
.application .theme--light.list,
.theme--light .list {
background: #fff;
color: rgba(0, 0, 0, .87)
}
.application .theme--light.list .list__tile__sub-title,
.theme--light .list .list__tile__sub-title {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.list .list__tile__mask,
.theme--light .list .list__tile__mask {
color: rgba(0, 0, 0, .38);
background: #eee
}
.application .theme--light.list .list__group--active:after,
.application .theme--light.list .list__group--active:before,
.application .theme--light.list .list__group__header:hover,
.application .theme--light.list .list__tile--highlighted,
.application .theme--light.list .list__tile--link:hover,
.theme--light .list .list__group--active:after,
.theme--light .list .list__group--active:before,
.theme--light .list .list__group__header:hover,
.theme--light .list .list__tile--highlighted,
.theme--light .list .list__tile--link:hover {
background: rgba(0, 0, 0, .12)
}
.application .theme--light.list .list__group--disabled .list__group__header__prepend-icon .icon,
.application .theme--light.list .list__group--disabled .list__tile,
.theme--light .list .list__group--disabled .list__group__header__prepend-icon .icon,
.theme--light .list .list__group--disabled .list__tile {
color: rgba(0, 0, 0, .38) !important
}
.application .theme--dark.list,
.theme--dark .list {
background: #424242;
color: #fff
}
.application .theme--dark.list .list__tile__sub-title,
.theme--dark .list .list__tile__sub-title {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.list .list__tile__mask,
.theme--dark .list .list__tile__mask {
color: hsla(0, 0%, 100%, .5);
background: rgba(0, 0, 0, .7)
}
.application .theme--dark.list .list__group--active:after,
.application .theme--dark.list .list__group--active:before,
.application .theme--dark.list .list__group__header:hover,
.application .theme--dark.list .list__tile--highlighted,
.application .theme--dark.list .list__tile--link:hover,
.theme--dark .list .list__group--active:after,
.theme--dark .list .list__group--active:before,
.theme--dark .list .list__group__header:hover,
.theme--dark .list .list__tile--highlighted,
.theme--dark .list .list__tile--link:hover {
background: hsla(0, 0%, 100%, .12)
}
.application .theme--dark.list .list__group--disabled .list__group__header__prepend-icon .icon,
.application .theme--dark.list .list__group--disabled .list__tile,
.theme--dark .list .list__group--disabled .list__group__header__prepend-icon .icon,
.theme--dark .list .list__group--disabled .list__tile {
color: hsla(0, 0%, 100%, .5) !important
}
.list {
list-style-type: none;
padding: 8px 0;
transition: height .3s cubic-bezier(.4, 0, .2, 1)
}
.list .input-group {
margin: 0
}
.list__tile {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: inherit;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 16px;
font-weight: 400;
height: 48px;
margin: 0;
padding: 0 16px;
position: relative;
text-decoration: none;
transition: .3s cubic-bezier(.25, .8, .5, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.list__tile--link {
cursor: pointer
}
.list__tile__action,
.list__tile__content {
height: 100%
}
.list__tile__sub-title,
.list__tile__title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%
}
.list__tile__title {
height: 24px;
line-height: 24px;
position: relative;
text-align: left
}
.list__tile__sub-title {
font-size: 14px
}
.list__tile__action,
.list__tile__avatar {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
min-width: 56px
}
.list__tile__action,
.list__tile__action .input-group--selection-controls {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.list__tile__action .input-group--selection-controls {
-webkit-box-flex: 0;
-ms-flex: 0 1;
flex: 0 1;
padding: 0
}
.list__tile__action .input-group__details {
display: none
}
.list__tile__action .btn {
padding: 0;
margin: 0
}
.list__tile__action .btn--icon {
margin: -8px
}
.list__tile__action-text {
color: #9e9e9e;
font-size: 12px
}
.list__tile__action--stack {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
padding-top: 8px;
padding-bottom: 8px;
white-space: nowrap;
-ms-flex-direction: column;
flex-direction: column
}
.list__tile__action--stack,
.list__tile__content {
-webkit-box-orient: vertical;
-webkit-box-direction: normal
}
.list__tile__content {
text-align: left;
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
overflow: hidden;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-direction: column;
flex-direction: column
}
.list__tile__content~.list__tile__action:not(.list__tile__action--stack),
.list__tile__content~.list__tile__avatar {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end
}
.list__tile--active .list__tile__action:first-of-type .icon {
color: inherit
}
.list__tile--disabled {
opacity: .4 !important;
pointer-events: none
}
.list__tile--avatar {
height: 56px
}
.list--dense {
padding-top: 4px;
padding-bottom: 4px
}
.list--dense .subheader {
font-size: 13px;
height: 40px
}
.list--dense .list__group .subheader {
height: 40px
}
.list--dense .list__tile {
font-size: 13px
}
.list--dense .list__tile--avatar {
height: 48px
}
.list--dense .list__tile:not(.list__tile--avatar) {
height: 40px
}
.list--dense .list__tile .icon {
font-size: 22px
}
.list--dense .list__tile__sub-title {
font-size: 13px
}
.list--two-line .list__tile {
height: 72px
}
.list--two-line.list--dense .list__tile {
height: 60px
}
.list--three-line .list__tile {
height: 88px
}
.list--three-line .list__tile__avatar {
margin-top: -18px
}
.list--three-line .list__tile__sub-title {
white-space: normal;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: -webkit-box
}
.list--three-line.list--dense .list__tile {
height: 76px
}
.list>.list__group:before {
top: 0
}
.list>.list__group:before .list__tile__avatar {
margin-top: -14px
}
.list__group {
padding: 0;
position: relative;
transition: inherit
}
.list__group:after,
.list__group:before {
content: "";
height: 1px;
position: absolute;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%
}
.list__group--active~.list__group:before {
display: none
}
.list__group__header {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
list-style-type: none
}
.list__group__header>li:not(.list__group__header__prepend-icon):not(.list__group__header__append-icon) {
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto
}
.list__group__header .list__group__header__append-icon,
.list__group__header .list__group__header__prepend-icon {
padding: 0 16px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.list__group__header--sub-group {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.list__group__header--sub-group li .list__tile {
padding-left: 0
}
.list__group__header--sub-group .list__group__header__prepend-icon {
padding: 0 0 0 40px;
margin-right: 8px
}
.list__group__header .list__group__header__prepend-icon {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
min-width: 56px
}
.list__group__header--active .list__group__header__append-icon .icon {
-webkit-transform: rotate(-180deg);
transform: rotate(-180deg)
}
.list__group__header--active .list__group__header__prepend-icon .icon {
color: inherit
}
.list__group__header--active.list__group__header--sub-group .list__group__header__prepend-icon .icon {
-webkit-transform: rotate(-180deg);
transform: rotate(-180deg)
}
.list__group__items {
position: relative;
padding: 0;
transition: inherit
}
.list__group__items>li {
display: block
}
.list__group__items--no-action .list__tile {
padding-left: 72px
}
.list__group--disabled {
pointer-events: none
}
.list--subheader {
padding-top: 0
}
.menu {
display: inline-block;
position: relative;
vertical-align: middle
}
.menu--disabled {
cursor: default
}
.menu--disabled .menu__activator,
.menu--disabled .menu__activator>* {
cursor: default;
pointer-events: none
}
.menu__activator {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
height: inherit;
position: relative
}
.menu__activator input[readonly] {
cursor: pointer
}
.menu__content {
position: absolute;
display: inline-block;
border-radius: 2px;
max-width: 80%;
overflow-y: auto;
overflow-x: hidden;
contain: content;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12)
}
.menu__content--active {
pointer-events: none
}
.menu__content--dropdown {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 1px solid rgba(0, 0, 0, .12)
}
.menu__content>.card {
contain: content;
-webkit-backface-visibility: hidden;
backface-visibility: hidden
}
.menu>.menu__content {
max-width: none
}
.menu-transition-enter .list__tile {
min-width: 0;
transition-delay: .4s;
opacity: 0;
-webkit-transform: translateY(-15px);
transform: translateY(-15px);
pointer-events: none
}
.menu-transition-enter-to .list__tile {
pointer-events: auto;
opacity: 1
}
.menu-transition-enter-to .list__tile--active {
-webkit-transform: none !important;
transform: none !important
}
.menu-transition-leave-to {
-webkit-transform: translateY(-10px);
transform: translateY(-10px)
}
.menu-transition-leave-active,
.menu-transition-leave-to {
pointer-events: none
}
.menu-transition-enter,
.menu-transition-leave-to {
opacity: 0
}
.menu-transition-enter-to,
.menu-transition-leave {
opacity: 1
}
.menu-transition-enter-active,
.menu-transition-leave-active {
transition: all .5s cubic-bezier(.25, .8, .5, 1)
}
.menu-transition-enter.menu__content--auto .list__tile--active {
opacity: 1;
-webkit-transform: none !important;
transform: none !important;
pointer-events: auto
}
.application .theme--light.table,
.theme--light .table {
background-color: #fff;
color: rgba(0, 0, 0, .87)
}
.application .theme--light.table thead tr:first-child,
.theme--light .table thead tr:first-child {
border-bottom: 1px solid rgba(0, 0, 0, .12)
}
.application .theme--light.table thead th,
.theme--light .table thead th {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.table tbody tr:not(:last-child),
.theme--light .table tbody tr:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, .12)
}
.application .theme--light.table tbody tr[active],
.theme--light .table tbody tr[active] {
background: #f5f5f5
}
.application .theme--light.table tbody tr:hover:not(.datatable__expand-row),
.theme--light .table tbody tr:hover:not(.datatable__expand-row) {
background: #eee
}
.application .theme--light.table tfoot tr,
.theme--light .table tfoot tr {
border-top: 1px solid rgba(0, 0, 0, .12)
}
.application .theme--dark.table,
.theme--dark .table {
background-color: #424242;
color: #fff
}
.application .theme--dark.table thead tr:first-child,
.theme--dark .table thead tr:first-child {
border-bottom: 1px solid hsla(0, 0%, 100%, .12)
}
.application .theme--dark.table thead th,
.theme--dark .table thead th {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.table tbody tr:not(:last-child),
.theme--dark .table tbody tr:not(:last-child) {
border-bottom: 1px solid hsla(0, 0%, 100%, .12)
}
.application .theme--dark.table tbody tr[active],
.theme--dark .table tbody tr[active] {
background: #505050
}
.application .theme--dark.table tbody tr:hover:not(.datatable__expand-row),
.theme--dark .table tbody tr:hover:not(.datatable__expand-row) {
background: #616161
}
.application .theme--dark.table tfoot tr,
.theme--dark .table tfoot tr {
border-top: 1px solid hsla(0, 0%, 100%, .12)
}
.table__overflow {
width: 100%;
overflow-x: auto;
overflow-y: hidden
}
table.table {
border-radius: 2px;
border-collapse: collapse;
border-spacing: 0;
width: 100%;
max-width: 100%
}
table.table tbody td:first-child,
table.table tbody td:not(:first-child),
table.table tbody th:first-child,
table.table tbody th:not(:first-child),
table.table thead td:first-child,
table.table thead td:not(:first-child),
table.table thead th:first-child,
table.table thead th:not(:first-child) {
padding: 0 24px
}
table.table thead tr {
height: 56px
}
table.table thead th {
font-weight: 500;
font-size: 12px;
transition: .3s cubic-bezier(.25, .8, .5, 1);
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
table.table thead th.sortable {
pointer-events: auto
}
table.table thead th>div {
width: 100%
}
table.table tbody tr {
transition: background .3s cubic-bezier(.25, .8, .5, 1);
will-change: background
}
table.table tbody td,
table.table tbody th {
height: 48px
}
table.table tbody td {
font-weight: 400;
font-size: 13px
}
table.table .input-group--selection-controls {
padding: 0
}
table.table .input-group--selection-controls .input-group__details {
display: none
}
table.table .input-group--selection-controls.checkbox .icon {
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%)
}
table.table .input-group--selection-controls.checkbox .input-group--selection-controls__ripple {
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%)
}
table.table tfoot tr {
height: 48px
}
table.table tfoot tr td {
padding: 0 24px
}
.application .theme--light.datatable thead th.column.sortable i,
.theme--light .datatable thead th.column.sortable i {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.datatable thead th.column.sortable.active,
.application .theme--light.datatable thead th.column.sortable.active i,
.application .theme--light.datatable thead th.column.sortable:hover,
.theme--light .datatable thead th.column.sortable.active,
.theme--light .datatable thead th.column.sortable.active i,
.theme--light .datatable thead th.column.sortable:hover {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.datatable .datatable__actions,
.theme--light .datatable .datatable__actions {
background-color: #fff;
color: rgba(0, 0, 0, .54);
border-top: 1px solid rgba(0, 0, 0, .12)
}
.application .theme--light.datatable .datatable__actions__select .input-group--select .input-group__append-icon,
.application .theme--light.datatable .datatable__actions__select .input-group--select .input-group__selections__comma,
.theme--light .datatable .datatable__actions__select .input-group--select .input-group__append-icon,
.theme--light .datatable .datatable__actions__select .input-group--select .input-group__selections__comma {
color: rgba(0, 0, 0, .54) !important
}
.application .theme--dark.datatable thead th.column.sortable i,
.theme--dark .datatable thead th.column.sortable i {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.datatable thead th.column.sortable.active,
.application .theme--dark.datatable thead th.column.sortable.active i,
.application .theme--dark.datatable thead th.column.sortable:hover,
.theme--dark .datatable thead th.column.sortable.active,
.theme--dark .datatable thead th.column.sortable.active i,
.theme--dark .datatable thead th.column.sortable:hover {
color: #fff
}
.application .theme--dark.datatable .datatable__actions,
.theme--dark .datatable .datatable__actions {
background-color: #424242;
color: hsla(0, 0%, 100%, .7);
border-top: 1px solid hsla(0, 0%, 100%, .12)
}
.application .theme--dark.datatable .datatable__actions__select .input-group--select .input-group__append-icon,
.application .theme--dark.datatable .datatable__actions__select .input-group--select .input-group__selections__comma,
.theme--dark .datatable .datatable__actions__select .input-group--select .input-group__append-icon,
.theme--dark .datatable .datatable__actions__select .input-group--select .input-group__selections__comma {
color: hsla(0, 0%, 100%, .7) !important
}
.datatable thead th.column.sortable {
cursor: pointer
}
.datatable thead th.column.sortable i {
font-size: 16px;
vertical-align: sub;
display: inline-block;
opacity: 0;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.datatable thead th.column.sortable:hover i {
opacity: .6
}
.datatable thead th.column.sortable.active {
-webkit-transform: none;
transform: none
}
.datatable thead th.column.sortable.active i {
opacity: 1
}
.datatable thead th.column.sortable.active.desc i {
-webkit-transform: rotate(-180deg);
transform: rotate(-180deg)
}
.datatable__actions {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 12px;
-ms-flex-wrap: wrap-reverse;
flex-wrap: wrap-reverse
}
.datatable__actions .btn {
color: inherit
}
.datatable__actions .btn:last-of-type {
margin-left: 14px
}
.datatable__actions__range-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
min-height: 48px
}
.datatable__actions__pagination {
display: block;
text-align: center;
margin: 0 32px 0 24px
}
.datatable__actions__select {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
margin-right: 14px
}
.datatable__actions__select .input-group--select {
margin: 13px 0 13px 34px;
padding: 0;
position: static
}
.datatable__actions__select .input-group--select .input-group__selections__comma {
font-size: 12px
}
.datatable__progress,
.datatable__progress td,
.datatable__progress th,
.datatable__progress tr {
height: auto !important
}
.datatable__progress th {
padding: 0 !important
}
.datatable__progress th .progress-linear {
top: -2px;
margin: 0 0 -2px
}
.datatable__expand-row {
border: none !important
}
.datatable__expand-col {
padding: 0 !important;
height: 0 !important
}
.datatable__expand-col--expanded {
border-bottom: 1px solid rgba(0, 0, 0, .12)
}
.datatable__expand-content {
transition: height .3s cubic-bezier(.25, .8, .5, 1)
}
.datatable__expand-content>.card {
border-radius: 0;
box-shadow: none
}
.progress-linear {
background: transparent;
margin: 1rem 0;
overflow: hidden;
width: 100%;
position: relative
}
.progress-linear__bar {
width: 100%;
position: relative;
z-index: 1
}
.progress-linear__bar,
.progress-linear__bar__determinate {
height: inherit;
transition: .2s
}
.progress-linear__bar__indeterminate .long,
.progress-linear__bar__indeterminate .short {
height: inherit;
position: absolute;
left: 0;
top: 0;
bottom: 0;
will-change: left, right;
width: auto;
background-color: inherit
}
.progress-linear__bar__indeterminate--active .long {
-webkit-animation: b;
animation: b;
-webkit-animation-duration: 2.2s;
animation-duration: 2.2s;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
.progress-linear__bar__indeterminate--active .short {
-webkit-animation: c;
animation: c;
-webkit-animation-duration: 2.2s;
animation-duration: 2.2s;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
.progress-linear__background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
transition: .3s ease-in
}
.progress-linear--query .progress-linear__bar__indeterminate--active .long {
-webkit-animation: d;
animation: d;
-webkit-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
.progress-linear--query .progress-linear__bar__indeterminate--active .short {
-webkit-animation: e;
animation: e;
-webkit-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
@-webkit-keyframes b {
0% {
left: -90%;
right: 100%
}
60% {
left: -90%;
right: 100%
}
to {
left: 100%;
right: -35%
}
}
@keyframes b {
0% {
left: -90%;
right: 100%
}
60% {
left: -90%;
right: 100%
}
to {
left: 100%;
right: -35%
}
}
@-webkit-keyframes c {
0% {
left: -200%;
right: 100%
}
60% {
left: 107%;
right: -8%
}
to {
left: 107%;
right: -8%
}
}
@keyframes c {
0% {
left: -200%;
right: 100%
}
60% {
left: 107%;
right: -8%
}
to {
left: 107%;
right: -8%
}
}
@-webkit-keyframes d {
0% {
right: -90%;
left: 100%
}
60% {
right: -90%;
left: 100%
}
to {
right: 100%;
left: -35%
}
}
@keyframes d {
0% {
right: -90%;
left: 100%
}
60% {
right: -90%;
left: 100%
}
to {
right: 100%;
left: -35%
}
}
@-webkit-keyframes e {
0% {
right: -200%;
left: 100%
}
60% {
right: 107%;
left: -8%
}
to {
right: 107%;
left: -8%
}
}
@keyframes e {
0% {
right: -200%;
left: 100%
}
60% {
right: 107%;
left: -8%
}
to {
right: 107%;
left: -8%
}
}
.application .theme--light.small-dialog__actions,
.application .theme--light.small-dialog__content,
.theme--light .small-dialog__actions,
.theme--light .small-dialog__content {
background: #fff
}
.application .theme--light.small-dialog a,
.theme--light .small-dialog a {
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.small-dialog__actions,
.application .theme--dark.small-dialog__content,
.theme--dark .small-dialog__actions,
.theme--dark .small-dialog__content {
background: #424242
}
.application .theme--dark.small-dialog a,
.theme--dark .small-dialog a {
color: #fff
}
.small-dialog {
display: block;
height: 100%
}
.small-dialog__content {
padding: 0 24px
}
.small-dialog__actions {
text-align: right
}
.small-dialog a {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
height: 100%;
text-decoration: none
}
.small-dialog a>* {
width: 100%
}
.small-dialog .menu__activator {
height: 100%
}
.date-picker-title {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
line-height: 1
}
.date-picker-title__year {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px
}
.date-picker-title__date {
font-size: 34px;
text-align: left;
font-weight: 500;
position: relative;
overflow: hidden
}
.date-picker-title__date>div {
position: relative
}
.application .theme--light.date-picker-header .date-picker-header__value:not(.date-picker-header__value--disabled) strong:not(:hover),
.theme--light .date-picker-header .date-picker-header__value:not(.date-picker-header__value--disabled) strong:not(:hover) {
color: rgba(0, 0, 0, .87) !important
}
.application .theme--light.date-picker-header .date-picker-header__value--disabled strong,
.theme--light .date-picker-header .date-picker-header__value--disabled strong {
color: rgba(0, 0, 0, .38)
}
.application .theme--dark.date-picker-header .date-picker-header__value:not(.date-picker-header__value--disabled) strong:not(:hover),
.theme--dark .date-picker-header .date-picker-header__value:not(.date-picker-header__value--disabled) strong:not(:hover) {
color: #fff !important
}
.application .theme--dark.date-picker-header .date-picker-header__value--disabled strong,
.theme--dark .date-picker-header .date-picker-header__value--disabled strong {
color: hsla(0, 0%, 100%, .5)
}
.date-picker-header {
padding: 4px 16px;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
position: relative
}
.date-picker-header .btn {
margin: 0;
z-index: auto
}
.date-picker-header .icon {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.date-picker-header__value {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
text-align: center;
position: relative;
overflow: hidden
}
.date-picker-header__value strong {
cursor: pointer;
transition: .3s cubic-bezier(.25, .8, .5, 1);
display: block;
width: 100%
}
.application .theme--light.date-picker-table th,
.theme--light .date-picker-table th {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.date-picker-table .btn,
.theme--light .date-picker-table .btn {
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.date-picker-table th,
.theme--dark .date-picker-table th {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.date-picker-table .btn,
.theme--dark .date-picker-table .btn {
color: #fff
}
.date-picker-table {
position: relative;
padding: 0 12px;
height: 242px
}
.date-picker-table table {
transition: .3s cubic-bezier(.25, .8, .5, 1);
top: 0;
table-layout: fixed;
width: 100%
}
.date-picker-table td,
.date-picker-table th {
text-align: center;
position: relative
}
.date-picker-table th {
font-size: 12px
}
.date-picker-table--date .btn {
height: 32px;
width: 32px
}
.date-picker-table .btn {
z-index: auto;
margin: 0;
font-size: 12px
}
.date-picker-table .btn.btn--active {
color: #fff
}
.date-picker-table--month td {
width: 33.333333%;
height: 56px;
vertical-align: middle;
text-align: center
}
.date-picker-table--month td .btn {
margin: 0 auto;
max-width: 160px;
min-width: 40px;
width: 100%
}
.date-picker-table--date th {
padding: 8px 0;
font-weight: 600
}
.date-picker-table--date td {
width: 45px
}
.date-picker-table__event {
border-radius: 50%;
bottom: 2px;
content: "";
display: block;
height: 8px;
left: 50%;
position: absolute;
-webkit-transform: translateX(-4px);
transform: translateX(-4px);
width: 8px
}
.date-picker-years {
font-size: 16px;
font-weight: 400;
height: 334px;
list-style-type: none;
overflow: auto;
padding: 0;
text-align: center
}
.date-picker-years li {
cursor: pointer;
padding: 8px 0;
transition: none
}
.date-picker-years li.active {
font-size: 26px;
font-weight: 500;
padding: 10px 0
}
.date-picker-years li:hover {
background: rgba(0, 0, 0, .12)
}
.picker--landscape .date-picker-years {
height: 286px
}
.application .theme--dark.picker,
.theme--dark .picker {
color: #fff
}
.application .theme--dark.picker .picker__body,
.theme--dark .picker .picker__body {
background: #424242
}
.application .theme--light.picker .picker__title,
.theme--light .picker .picker__title {
background: #e0e0e0
}
.application .theme--dark.picker .picker__title,
.theme--dark .picker .picker__title {
background: #616161
}
.picker {
border-radius: 2px;
contain: layout style;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
vertical-align: top
}
.picker .card__row--actions {
border: none;
margin-top: -20px
}
.picker--full-width {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.picker__title {
color: #fff;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
padding: 16px
}
.picker__title__btn {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.picker__title__btn.active {
opacity: 1
}
.picker__title__btn:not(.active) {
opacity: .6;
cursor: pointer
}
.picker__title__btn:not(.active):hover {
opacity: 1
}
.picker__body {
height: auto;
overflow: hidden;
position: relative;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.picker__body>div {
width: 100%
}
.picker__body>div.fade-transition-leave-active {
position: absolute
}
.picker--landscape .picker__title {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: 170px;
position: absolute;
top: 0;
left: 0;
height: 100%;
z-index: 1
}
.picker--landscape .picker__actions,
.picker--landscape .picker__body {
margin-left: 170px
}
.application .theme--light.divider,
.theme--light .divider {
background-color: rgba(0, 0, 0, .12)
}
.application .theme--dark.divider,
.theme--dark .divider {
background-color: hsla(0, 0%, 100%, .12)
}
.divider {
border: none;
display: block;
height: 1px;
min-height: 1px;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
width: 100%
}
.divider--inset {
margin-left: 72px;
width: calc(100% - 72px)
}
.application .theme--light.expansion-panel .expansion-panel__container,
.theme--light .expansion-panel .expansion-panel__container {
border-top: 1px solid rgba(0, 0, 0, .12);
background-color: #fff;
color: rgba(0, 0, 0, .87)
}
.application .theme--light.expansion-panel .expansion-panel__container .expansion-panel__header .icon,
.theme--light .expansion-panel .expansion-panel__container .expansion-panel__header .icon {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.expansion-panel--focusable .expansion-panel__container:focus,
.theme--light .expansion-panel--focusable .expansion-panel__container:focus {
background-color: #eee
}
.application .theme--dark.expansion-panel .expansion-panel__container,
.theme--dark .expansion-panel .expansion-panel__container {
border-top: 1px solid hsla(0, 0%, 100%, .12);
background-color: #424242;
color: #fff
}
.application .theme--dark.expansion-panel .expansion-panel__container .expansion-panel__header .icon,
.theme--dark .expansion-panel .expansion-panel__container .expansion-panel__header .icon {
color: #fff
}
.application .theme--dark.expansion-panel--focusable .expansion-panel__container:focus,
.theme--dark .expansion-panel--focusable .expansion-panel__container:focus {
background-color: rgba(0, 0, 0, .7)
}
.expansion-panel {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
list-style-type: none;
padding: 0;
text-align: left;
width: 100%;
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12)
}
.expansion-panel__container {
-webkit-box-flex: 1;
-ms-flex: 1 0 100%;
flex: 1 0 100%;
max-width: 100%;
outline: none;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.expansion-panel__container:first-child {
border-top: none !important
}
.expansion-panel__container .header__icon {
margin-left: auto
}
.expansion-panel__container .header__icon .icon {
transition: none
}
.expansion-panel__container--active>.expansion-panel__header .header__icon .icon {
-webkit-transform: rotate(-180deg);
transform: rotate(-180deg)
}
.expansion-panel__header {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
cursor: pointer;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
padding: 12px 24px
}
.expansion-panel__header>:not(.header__icon) {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto
}
.expansion-panel__body {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.expansion-panel__body .card {
border-radius: 0
}
.expansion-panel--inset,
.expansion-panel--popout,
.expansion-panel__body .card {
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12)
}
.expansion-panel--inset .expansion-panel__container--active,
.expansion-panel--popout .expansion-panel__container--active {
margin: 16px;
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, .2), 0 3px 4px 0 rgba(0, 0, 0, .14), 0 1px 8px 0 rgba(0, 0, 0, .12)
}
.expansion-panel--inset .expansion-panel__container,
.expansion-panel--popout .expansion-panel__container {
max-width: 95%
}
.expansion-panel--popout .expansion-panel__container--active {
max-width: 100%
}
.expansion-panel--inset .expansion-panel__container--active {
max-width: 85%
}
.application .theme--light.footer,
.theme--light .footer {
background: #f5f5f5;
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.footer,
.theme--dark .footer {
background: #212121;
color: #fff
}
.footer {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 0 !important;
-ms-flex: 0 1 auto !important;
flex: 0 1 auto !important;
min-height: 36px;
transition: .2s cubic-bezier(.4, 0, .2, 1)
}
.footer--absolute,
.footer--fixed {
bottom: 0;
left: 0;
width: 100%;
z-index: 3
}
.footer--inset {
z-index: 2
}
.footer--absolute {
position: absolute
}
.footer--fixed {
position: fixed
}
.content {
transition: none;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
max-width: 100%;
will-change: padding
}
.content[data-booted=true] {
transition: .2s cubic-bezier(.4, 0, .2, 1)
}
.content--wrap {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
max-width: 100%
}
@-moz-document url-prefix() {
@media print {
.content {
display: block
}
}
}
.container {
-webkit-box-flex: 1;
-ms-flex: 1 1 100%;
flex: 1 1 100%;
margin: auto;
padding: 16px;
width: 100%
}
.container.fluid {
max-width: 100%
}
.container.fill-height {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.container.fill-height .layout {
height: 100%;
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto
}
.container.grid-list-xs {
padding: 2px
}
.container.grid-list-xs .layout .flex {
padding: 1px
}
.container.grid-list-xs .layout:only-child {
margin: -1px
}
.container.grid-list-xs .layout:not(:only-child) {
margin: auto -1px
}
.container.grid-list-xs :not(:only-child) .layout:first-child {
margin-top: -1px
}
.container.grid-list-xs :not(:only-child) .layout:last-child {
margin-bottom: -1px
}
.container.grid-list-sm {
padding: 4px
}
.container.grid-list-sm .layout .flex {
padding: 2px
}
.container.grid-list-sm .layout:only-child {
margin: -2px
}
.container.grid-list-sm .layout:not(:only-child) {
margin: auto -2px
}
.container.grid-list-sm :not(:only-child) .layout:first-child {
margin-top: -2px
}
.container.grid-list-sm :not(:only-child) .layout:last-child {
margin-bottom: -2px
}
.container.grid-list-md {
padding: 8px
}
.container.grid-list-md .layout .flex {
padding: 4px
}
.container.grid-list-md .layout:only-child {
margin: -4px
}
.container.grid-list-md .layout:not(:only-child) {
margin: auto -4px
}
.container.grid-list-md :not(:only-child) .layout:first-child {
margin-top: -4px
}
.container.grid-list-md :not(:only-child) .layout:last-child {
margin-bottom: -4px
}
.container.grid-list-lg {
padding: 16px
}
.container.grid-list-lg .layout .flex {
padding: 8px
}
.container.grid-list-lg .layout:only-child {
margin: -8px
}
.container.grid-list-lg .layout:not(:only-child) {
margin: auto -8px
}
.container.grid-list-lg :not(:only-child) .layout:first-child {
margin-top: -8px
}
.container.grid-list-lg :not(:only-child) .layout:last-child {
margin-bottom: -8px
}
.container.grid-list-xl {
padding: 24px
}
.container.grid-list-xl .layout .flex {
padding: 12px
}
.container.grid-list-xl .layout:only-child {
margin: -12px
}
.container.grid-list-xl .layout:not(:only-child) {
margin: auto -12px
}
.container.grid-list-xl :not(:only-child) .layout:first-child {
margin-top: -12px
}
.container.grid-list-xl :not(:only-child) .layout:last-child {
margin-bottom: -12px
}
.layout {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap
}
.layout.row {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row
}
.layout.row.reverse {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse
}
.layout.column {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column
}
.layout.column.reverse {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse
}
.layout.wrap {
-ms-flex-wrap: wrap;
flex-wrap: wrap
}
.child-flex>*,
.flex {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto
}
.align-start {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start
}
.align-end {
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end
}
.align-center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.align-baseline {
-webkit-box-align: baseline;
-ms-flex-align: baseline;
align-items: baseline
}
.align-content-start {
-ms-flex-line-pack: start;
align-content: flex-start
}
.align-content-end {
-ms-flex-line-pack: end;
align-content: flex-end
}
.align-content-center {
-ms-flex-line-pack: center;
align-content: center
}
.align-content-space-between {
-ms-flex-line-pack: justify;
align-content: space-between
}
.align-content-space-around {
-ms-flex-line-pack: distribute;
align-content: space-around
}
.justify-start {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start
}
.justify-end {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end
}
.justify-center {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center
}
.justify-space-around {
-ms-flex-pack: distribute;
justify-content: space-around
}
.justify-space-between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between
}
.spacer {
-webkit-box-flex: 1 !important;
-ms-flex-positive: 1 !important;
flex-grow: 1 !important
}
.grow {
-ms-flex-negative: 0 !important;
flex-shrink: 0 !important
}
.shrink {
-webkit-box-flex: 0 !important;
-ms-flex-positive: 0 !important;
flex-grow: 0 !important
}
.scroll-y {
overflow-y: auto
}
.fill-height {
height: 100%
}
.hide-overflow {
overflow: hidden !important
}
.show-overflow {
overflow: visible !important
}
.ellipsis,
.no-wrap {
white-space: nowrap
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis
}
.d-flex {
display: -webkit-box !important;
display: -ms-flexbox !important;
display: flex !important
}
.d-inline-flex {
display: -webkit-inline-box !important;
display: -ms-inline-flexbox !important;
display: inline-flex !important
}
.d-flex>*,
.d-inline-flex>* {
-webkit-box-flex: 1 !important;
-ms-flex: 1 1 auto !important;
flex: 1 1 auto !important
}
.d-block {
display: block !important
}
.d-inline-block {
display: inline-block !important
}
@media only screen and (min-width:960px) {
.container {
max-width: 900px
}
}
@media only screen and (min-width:1264px) {
.container {
max-width: 1185px
}
}
@media only screen and (min-width:1904px) {
.container {
max-width: 1785px
}
}
@media only screen and (max-width:599px) {
.container {
padding: 24px
}
}
@media (min-width:0) {
.flex.xs1 {
-ms-flex-preferred-size: 8.333333333333332%;
flex-basis: 8.333333333333332%;
max-width: 8.333333333333332%
}
.flex.order-xs1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1
}
.flex.xs2 {
-ms-flex-preferred-size: 16.666666666666664%;
flex-basis: 16.666666666666664%;
max-width: 16.666666666666664%
}
.flex.order-xs2 {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2
}
.flex.xs3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%
}
.flex.order-xs3 {
-webkit-box-ordinal-group: 4;
-ms-flex-order: 3;
order: 3
}
.flex.xs4 {
-ms-flex-preferred-size: 33.33333333333333%;
flex-basis: 33.33333333333333%;
max-width: 33.33333333333333%
}
.flex.order-xs4 {
-webkit-box-ordinal-group: 5;
-ms-flex-order: 4;
order: 4
}
.flex.xs5 {
-ms-flex-preferred-size: 41.66666666666667%;
flex-basis: 41.66666666666667%;
max-width: 41.66666666666667%
}
.flex.order-xs5 {
-webkit-box-ordinal-group: 6;
-ms-flex-order: 5;
order: 5
}
.flex.xs6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%
}
.flex.order-xs6 {
-webkit-box-ordinal-group: 7;
-ms-flex-order: 6;
order: 6
}
.flex.xs7 {
-ms-flex-preferred-size: 58.333333333333336%;
flex-basis: 58.333333333333336%;
max-width: 58.333333333333336%
}
.flex.order-xs7 {
-webkit-box-ordinal-group: 8;
-ms-flex-order: 7;
order: 7
}
.flex.xs8 {
-ms-flex-preferred-size: 66.66666666666666%;
flex-basis: 66.66666666666666%;
max-width: 66.66666666666666%
}
.flex.order-xs8 {
-webkit-box-ordinal-group: 9;
-ms-flex-order: 8;
order: 8
}
.flex.xs9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%
}
.flex.order-xs9 {
-webkit-box-ordinal-group: 10;
-ms-flex-order: 9;
order: 9
}
.flex.xs10 {
-ms-flex-preferred-size: 83.33333333333334%;
flex-basis: 83.33333333333334%;
max-width: 83.33333333333334%
}
.flex.order-xs10 {
-webkit-box-ordinal-group: 11;
-ms-flex-order: 10;
order: 10
}
.flex.xs11 {
-ms-flex-preferred-size: 91.66666666666666%;
flex-basis: 91.66666666666666%;
max-width: 91.66666666666666%
}
.flex.order-xs11 {
-webkit-box-ordinal-group: 12;
-ms-flex-order: 11;
order: 11
}
.flex.xs12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%
}
.flex.order-xs12 {
-webkit-box-ordinal-group: 13;
-ms-flex-order: 12;
order: 12
}
.flex.offset-xs0 {
margin-left: 0
}
.flex.offset-xs1 {
margin-left: 8.333333333333332%
}
.flex.offset-xs2 {
margin-left: 16.666666666666664%
}
.flex.offset-xs3 {
margin-left: 25%
}
.flex.offset-xs4 {
margin-left: 33.33333333333333%
}
.flex.offset-xs5 {
margin-left: 41.66666666666667%
}
.flex.offset-xs6 {
margin-left: 50%
}
.flex.offset-xs7 {
margin-left: 58.333333333333336%
}
.flex.offset-xs8 {
margin-left: 66.66666666666666%
}
.flex.offset-xs9 {
margin-left: 75%
}
.flex.offset-xs10 {
margin-left: 83.33333333333334%
}
.flex.offset-xs11 {
margin-left: 91.66666666666666%
}
.flex.offset-xs12 {
margin-left: 100%
}
}
@media (min-width:600px) {
.flex.sm1 {
-ms-flex-preferred-size: 8.333333333333332%;
flex-basis: 8.333333333333332%;
max-width: 8.333333333333332%
}
.flex.order-sm1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1
}
.flex.sm2 {
-ms-flex-preferred-size: 16.666666666666664%;
flex-basis: 16.666666666666664%;
max-width: 16.666666666666664%
}
.flex.order-sm2 {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2
}
.flex.sm3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%
}
.flex.order-sm3 {
-webkit-box-ordinal-group: 4;
-ms-flex-order: 3;
order: 3
}
.flex.sm4 {
-ms-flex-preferred-size: 33.33333333333333%;
flex-basis: 33.33333333333333%;
max-width: 33.33333333333333%
}
.flex.order-sm4 {
-webkit-box-ordinal-group: 5;
-ms-flex-order: 4;
order: 4
}
.flex.sm5 {
-ms-flex-preferred-size: 41.66666666666667%;
flex-basis: 41.66666666666667%;
max-width: 41.66666666666667%
}
.flex.order-sm5 {
-webkit-box-ordinal-group: 6;
-ms-flex-order: 5;
order: 5
}
.flex.sm6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%
}
.flex.order-sm6 {
-webkit-box-ordinal-group: 7;
-ms-flex-order: 6;
order: 6
}
.flex.sm7 {
-ms-flex-preferred-size: 58.333333333333336%;
flex-basis: 58.333333333333336%;
max-width: 58.333333333333336%
}
.flex.order-sm7 {
-webkit-box-ordinal-group: 8;
-ms-flex-order: 7;
order: 7
}
.flex.sm8 {
-ms-flex-preferred-size: 66.66666666666666%;
flex-basis: 66.66666666666666%;
max-width: 66.66666666666666%
}
.flex.order-sm8 {
-webkit-box-ordinal-group: 9;
-ms-flex-order: 8;
order: 8
}
.flex.sm9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%
}
.flex.order-sm9 {
-webkit-box-ordinal-group: 10;
-ms-flex-order: 9;
order: 9
}
.flex.sm10 {
-ms-flex-preferred-size: 83.33333333333334%;
flex-basis: 83.33333333333334%;
max-width: 83.33333333333334%
}
.flex.order-sm10 {
-webkit-box-ordinal-group: 11;
-ms-flex-order: 10;
order: 10
}
.flex.sm11 {
-ms-flex-preferred-size: 91.66666666666666%;
flex-basis: 91.66666666666666%;
max-width: 91.66666666666666%
}
.flex.order-sm11 {
-webkit-box-ordinal-group: 12;
-ms-flex-order: 11;
order: 11
}
.flex.sm12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%
}
.flex.order-sm12 {
-webkit-box-ordinal-group: 13;
-ms-flex-order: 12;
order: 12
}
.flex.offset-sm0 {
margin-left: 0
}
.flex.offset-sm1 {
margin-left: 8.333333333333332%
}
.flex.offset-sm2 {
margin-left: 16.666666666666664%
}
.flex.offset-sm3 {
margin-left: 25%
}
.flex.offset-sm4 {
margin-left: 33.33333333333333%
}
.flex.offset-sm5 {
margin-left: 41.66666666666667%
}
.flex.offset-sm6 {
margin-left: 50%
}
.flex.offset-sm7 {
margin-left: 58.333333333333336%
}
.flex.offset-sm8 {
margin-left: 66.66666666666666%
}
.flex.offset-sm9 {
margin-left: 75%
}
.flex.offset-sm10 {
margin-left: 83.33333333333334%
}
.flex.offset-sm11 {
margin-left: 91.66666666666666%
}
.flex.offset-sm12 {
margin-left: 100%
}
}
@media (min-width:960px) {
.flex.md1 {
-ms-flex-preferred-size: 8.333333333333332%;
flex-basis: 8.333333333333332%;
max-width: 8.333333333333332%
}
.flex.order-md1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1
}
.flex.md2 {
-ms-flex-preferred-size: 16.666666666666664%;
flex-basis: 16.666666666666664%;
max-width: 16.666666666666664%
}
.flex.order-md2 {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2
}
.flex.md3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%
}
.flex.order-md3 {
-webkit-box-ordinal-group: 4;
-ms-flex-order: 3;
order: 3
}
.flex.md4 {
-ms-flex-preferred-size: 33.33333333333333%;
flex-basis: 33.33333333333333%;
max-width: 33.33333333333333%
}
.flex.order-md4 {
-webkit-box-ordinal-group: 5;
-ms-flex-order: 4;
order: 4
}
.flex.md5 {
-ms-flex-preferred-size: 41.66666666666667%;
flex-basis: 41.66666666666667%;
max-width: 41.66666666666667%
}
.flex.order-md5 {
-webkit-box-ordinal-group: 6;
-ms-flex-order: 5;
order: 5
}
.flex.md6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%
}
.flex.order-md6 {
-webkit-box-ordinal-group: 7;
-ms-flex-order: 6;
order: 6
}
.flex.md7 {
-ms-flex-preferred-size: 58.333333333333336%;
flex-basis: 58.333333333333336%;
max-width: 58.333333333333336%
}
.flex.order-md7 {
-webkit-box-ordinal-group: 8;
-ms-flex-order: 7;
order: 7
}
.flex.md8 {
-ms-flex-preferred-size: 66.66666666666666%;
flex-basis: 66.66666666666666%;
max-width: 66.66666666666666%
}
.flex.order-md8 {
-webkit-box-ordinal-group: 9;
-ms-flex-order: 8;
order: 8
}
.flex.md9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%
}
.flex.order-md9 {
-webkit-box-ordinal-group: 10;
-ms-flex-order: 9;
order: 9
}
.flex.md10 {
-ms-flex-preferred-size: 83.33333333333334%;
flex-basis: 83.33333333333334%;
max-width: 83.33333333333334%
}
.flex.order-md10 {
-webkit-box-ordinal-group: 11;
-ms-flex-order: 10;
order: 10
}
.flex.md11 {
-ms-flex-preferred-size: 91.66666666666666%;
flex-basis: 91.66666666666666%;
max-width: 91.66666666666666%
}
.flex.order-md11 {
-webkit-box-ordinal-group: 12;
-ms-flex-order: 11;
order: 11
}
.flex.md12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%
}
.flex.order-md12 {
-webkit-box-ordinal-group: 13;
-ms-flex-order: 12;
order: 12
}
.flex.offset-md0 {
margin-left: 0
}
.flex.offset-md1 {
margin-left: 8.333333333333332%
}
.flex.offset-md2 {
margin-left: 16.666666666666664%
}
.flex.offset-md3 {
margin-left: 25%
}
.flex.offset-md4 {
margin-left: 33.33333333333333%
}
.flex.offset-md5 {
margin-left: 41.66666666666667%
}
.flex.offset-md6 {
margin-left: 50%
}
.flex.offset-md7 {
margin-left: 58.333333333333336%
}
.flex.offset-md8 {
margin-left: 66.66666666666666%
}
.flex.offset-md9 {
margin-left: 75%
}
.flex.offset-md10 {
margin-left: 83.33333333333334%
}
.flex.offset-md11 {
margin-left: 91.66666666666666%
}
.flex.offset-md12 {
margin-left: 100%
}
}
@media (min-width:1264px) {
.flex.lg1 {
-ms-flex-preferred-size: 8.333333333333332%;
flex-basis: 8.333333333333332%;
max-width: 8.333333333333332%
}
.flex.order-lg1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1
}
.flex.lg2 {
-ms-flex-preferred-size: 16.666666666666664%;
flex-basis: 16.666666666666664%;
max-width: 16.666666666666664%
}
.flex.order-lg2 {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2
}
.flex.lg3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%
}
.flex.order-lg3 {
-webkit-box-ordinal-group: 4;
-ms-flex-order: 3;
order: 3
}
.flex.lg4 {
-ms-flex-preferred-size: 33.33333333333333%;
flex-basis: 33.33333333333333%;
max-width: 33.33333333333333%
}
.flex.order-lg4 {
-webkit-box-ordinal-group: 5;
-ms-flex-order: 4;
order: 4
}
.flex.lg5 {
-ms-flex-preferred-size: 41.66666666666667%;
flex-basis: 41.66666666666667%;
max-width: 41.66666666666667%
}
.flex.order-lg5 {
-webkit-box-ordinal-group: 6;
-ms-flex-order: 5;
order: 5
}
.flex.lg6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%
}
.flex.order-lg6 {
-webkit-box-ordinal-group: 7;
-ms-flex-order: 6;
order: 6
}
.flex.lg7 {
-ms-flex-preferred-size: 58.333333333333336%;
flex-basis: 58.333333333333336%;
max-width: 58.333333333333336%
}
.flex.order-lg7 {
-webkit-box-ordinal-group: 8;
-ms-flex-order: 7;
order: 7
}
.flex.lg8 {
-ms-flex-preferred-size: 66.66666666666666%;
flex-basis: 66.66666666666666%;
max-width: 66.66666666666666%
}
.flex.order-lg8 {
-webkit-box-ordinal-group: 9;
-ms-flex-order: 8;
order: 8
}
.flex.lg9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%
}
.flex.order-lg9 {
-webkit-box-ordinal-group: 10;
-ms-flex-order: 9;
order: 9
}
.flex.lg10 {
-ms-flex-preferred-size: 83.33333333333334%;
flex-basis: 83.33333333333334%;
max-width: 83.33333333333334%
}
.flex.order-lg10 {
-webkit-box-ordinal-group: 11;
-ms-flex-order: 10;
order: 10
}
.flex.lg11 {
-ms-flex-preferred-size: 91.66666666666666%;
flex-basis: 91.66666666666666%;
max-width: 91.66666666666666%
}
.flex.order-lg11 {
-webkit-box-ordinal-group: 12;
-ms-flex-order: 11;
order: 11
}
.flex.lg12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%
}
.flex.order-lg12 {
-webkit-box-ordinal-group: 13;
-ms-flex-order: 12;
order: 12
}
.flex.offset-lg0 {
margin-left: 0
}
.flex.offset-lg1 {
margin-left: 8.333333333333332%
}
.flex.offset-lg2 {
margin-left: 16.666666666666664%
}
.flex.offset-lg3 {
margin-left: 25%
}
.flex.offset-lg4 {
margin-left: 33.33333333333333%
}
.flex.offset-lg5 {
margin-left: 41.66666666666667%
}
.flex.offset-lg6 {
margin-left: 50%
}
.flex.offset-lg7 {
margin-left: 58.333333333333336%
}
.flex.offset-lg8 {
margin-left: 66.66666666666666%
}
.flex.offset-lg9 {
margin-left: 75%
}
.flex.offset-lg10 {
margin-left: 83.33333333333334%
}
.flex.offset-lg11 {
margin-left: 91.66666666666666%
}
.flex.offset-lg12 {
margin-left: 100%
}
}
@media (min-width:1904px) {
.flex.xl1 {
-ms-flex-preferred-size: 8.333333333333332%;
flex-basis: 8.333333333333332%;
max-width: 8.333333333333332%
}
.flex.order-xl1 {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1
}
.flex.xl2 {
-ms-flex-preferred-size: 16.666666666666664%;
flex-basis: 16.666666666666664%;
max-width: 16.666666666666664%
}
.flex.order-xl2 {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2
}
.flex.xl3 {
-ms-flex-preferred-size: 25%;
flex-basis: 25%;
max-width: 25%
}
.flex.order-xl3 {
-webkit-box-ordinal-group: 4;
-ms-flex-order: 3;
order: 3
}
.flex.xl4 {
-ms-flex-preferred-size: 33.33333333333333%;
flex-basis: 33.33333333333333%;
max-width: 33.33333333333333%
}
.flex.order-xl4 {
-webkit-box-ordinal-group: 5;
-ms-flex-order: 4;
order: 4
}
.flex.xl5 {
-ms-flex-preferred-size: 41.66666666666667%;
flex-basis: 41.66666666666667%;
max-width: 41.66666666666667%
}
.flex.order-xl5 {
-webkit-box-ordinal-group: 6;
-ms-flex-order: 5;
order: 5
}
.flex.xl6 {
-ms-flex-preferred-size: 50%;
flex-basis: 50%;
max-width: 50%
}
.flex.order-xl6 {
-webkit-box-ordinal-group: 7;
-ms-flex-order: 6;
order: 6
}
.flex.xl7 {
-ms-flex-preferred-size: 58.333333333333336%;
flex-basis: 58.333333333333336%;
max-width: 58.333333333333336%
}
.flex.order-xl7 {
-webkit-box-ordinal-group: 8;
-ms-flex-order: 7;
order: 7
}
.flex.xl8 {
-ms-flex-preferred-size: 66.66666666666666%;
flex-basis: 66.66666666666666%;
max-width: 66.66666666666666%
}
.flex.order-xl8 {
-webkit-box-ordinal-group: 9;
-ms-flex-order: 8;
order: 8
}
.flex.xl9 {
-ms-flex-preferred-size: 75%;
flex-basis: 75%;
max-width: 75%
}
.flex.order-xl9 {
-webkit-box-ordinal-group: 10;
-ms-flex-order: 9;
order: 9
}
.flex.xl10 {
-ms-flex-preferred-size: 83.33333333333334%;
flex-basis: 83.33333333333334%;
max-width: 83.33333333333334%
}
.flex.order-xl10 {
-webkit-box-ordinal-group: 11;
-ms-flex-order: 10;
order: 10
}
.flex.xl11 {
-ms-flex-preferred-size: 91.66666666666666%;
flex-basis: 91.66666666666666%;
max-width: 91.66666666666666%
}
.flex.order-xl11 {
-webkit-box-ordinal-group: 12;
-ms-flex-order: 11;
order: 11
}
.flex.xl12 {
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%
}
.flex.order-xl12 {
-webkit-box-ordinal-group: 13;
-ms-flex-order: 12;
order: 12
}
.flex.offset-xl0 {
margin-left: 0
}
.flex.offset-xl1 {
margin-left: 8.333333333333332%
}
.flex.offset-xl2 {
margin-left: 16.666666666666664%
}
.flex.offset-xl3 {
margin-left: 25%
}
.flex.offset-xl4 {
margin-left: 33.33333333333333%
}
.flex.offset-xl5 {
margin-left: 41.66666666666667%
}
.flex.offset-xl6 {
margin-left: 50%
}
.flex.offset-xl7 {
margin-left: 58.333333333333336%
}
.flex.offset-xl8 {
margin-left: 66.66666666666666%
}
.flex.offset-xl9 {
margin-left: 75%
}
.flex.offset-xl10 {
margin-left: 83.33333333333334%
}
.flex.offset-xl11 {
margin-left: 91.66666666666666%
}
.flex.offset-xl12 {
margin-left: 100%
}
}
.application .theme--light.navigation-drawer,
.theme--light .navigation-drawer {
background-color: #fff
}
.application .theme--light.navigation-drawer .divider,
.application .theme--light.navigation-drawer:not(.navigation-drawer--floating) .navigation-drawer__border,
.theme--light .navigation-drawer .divider,
.theme--light .navigation-drawer:not(.navigation-drawer--floating) .navigation-drawer__border {
background-color: rgba(0, 0, 0, .12)
}
.application .theme--dark.navigation-drawer,
.theme--dark .navigation-drawer {
background-color: #424242
}
.application .theme--dark.navigation-drawer .divider,
.application .theme--dark.navigation-drawer:not(.navigation-drawer--floating) .navigation-drawer__border,
.theme--dark .navigation-drawer .divider,
.theme--dark .navigation-drawer:not(.navigation-drawer--floating) .navigation-drawer__border {
background-color: hsla(0, 0%, 100%, .12)
}
.navigation-drawer {
transition: none;
display: block;
left: 0;
max-width: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 0 0 100px;
pointer-events: auto;
top: 0;
will-change: transform;
z-index: 3;
-webkit-overflow-scrolling: touch
}
.navigation-drawer[data-booted=true] {
transition: .2s cubic-bezier(.4, 0, .2, 1);
transition-property: background, background-color, border, border-bottom, border-bottom-color, border-bottom-width, border-color, border-left, border-left-color, border-left-width, border-right, border-right-color, border-right-width, border-top, border-top-color, border-top-width, border-width, bottom, box-shadow, color, height, left, margin, margin-bottom, margin-left, margin-right, margin-top, max-width, min-height, min-width, opacity, padding, padding-bottom, padding-left, padding-right, padding-top, right, top, transform, transform-origin, width;
transition-property: background, background-color, border, border-bottom, border-bottom-color, border-bottom-width, border-color, border-left, border-left-color, border-left-width, border-right, border-right-color, border-right-width, border-top, border-top-color, border-top-width, border-width, bottom, box-shadow, color, height, left, margin, margin-bottom, margin-left, margin-right, margin-top, max-width, min-height, min-width, opacity, padding, padding-bottom, padding-left, padding-right, padding-top, right, top, transform, transform-origin, width, -webkit-transform, -webkit-transform-origin
}
.navigation-drawer__border {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 1px
}
.navigation-drawer.navigation-drawer--right:after {
left: 0;
right: auto
}
.navigation-drawer--right {
left: auto;
right: 0
}
.navigation-drawer--right>.navigation-drawer__border {
right: auto;
left: 0
}
.navigation-drawer--absolute {
position: absolute
}
.navigation-drawer--fixed {
position: fixed
}
.navigation-drawer--floating:after {
display: none
}
.navigation-drawer--mini-variant {
overflow: hidden
}
.navigation-drawer--mini-variant .list__group__header__prepend-icon {
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
width: 100%
}
.navigation-drawer--mini-variant .list__tile__action,
.navigation-drawer--mini-variant .list__tile__avatar {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
min-width: 48px
}
.navigation-drawer--mini-variant .list__tile:after,
.navigation-drawer--mini-variant .list__tile__content {
opacity: 0
}
.navigation-drawer--mini-variant .divider,
.navigation-drawer--mini-variant .list--group,
.navigation-drawer--mini-variant .subheader {
display: none !important
}
.navigation-drawer--is-mobile,
.navigation-drawer--temporary {
z-index: 6
}
.navigation-drawer--is-mobile:not(.navigation-drawer--close),
.navigation-drawer--temporary:not(.navigation-drawer--close) {
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, .2), 0 16px 24px 2px rgba(0, 0, 0, .14), 0 6px 30px 5px rgba(0, 0, 0, .12)
}
.navigation-drawer .list {
background: inherit
}
.navigation-drawer>.list .list__tile {
transition: none;
font-weight: 500
}
.navigation-drawer>.list .list__tile--active .list__tile__title {
color: inherit
}
.navigation-drawer>.list .list--group .list__tile {
font-weight: 400
}
.navigation-drawer>.list .list--group__header--active:after {
background: transparent
}
.navigation-drawer>.list:not(.list--dense) .list__tile {
font-size: 14px
}
.application .theme--light.pagination__item,
.theme--light .pagination__item {
background: #fff;
color: #000
}
.application .theme--light.pagination__item--active,
.theme--light .pagination__item--active {
color: #fff
}
.application .theme--light.pagination__navigation,
.theme--light .pagination__navigation {
background: #fff
}
.application .theme--light.pagination__navigation .icon,
.theme--light .pagination__navigation .icon {
color: rgba(0, 0, 0, .54)
}
.application .theme--dark.pagination__item,
.theme--dark .pagination__item {
background: #424242;
color: #fff
}
.application .theme--dark.pagination__item--active,
.theme--dark .pagination__item--active {
color: #fff
}
.application .theme--dark.pagination__navigation,
.theme--dark .pagination__navigation {
background: #424242
}
.application .theme--dark.pagination__navigation .icon,
.theme--dark .pagination__navigation .icon {
color: #fff
}
.pagination {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
list-style-type: none;
margin: 0;
max-width: 100%;
padding: 0
}
.pagination,
.pagination>li {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.pagination>li {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.pagination--circle .pagination__item,
.pagination--circle .pagination__more,
.pagination--circle .pagination__navigation {
border-radius: 50%
}
.pagination--disabled {
pointer-events: none;
opacity: .6
}
.pagination__item {
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
border-radius: 4px;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 14px;
background: transparent;
height: 34px;
width: 34px;
margin: .3rem;
text-decoration: none;
transition: .3s cubic-bezier(0, 0, .2, 1)
}
.pagination__item--active {
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, .2), 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12)
}
.pagination__navigation {
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-decoration: none;
height: 2rem;
border-radius: 4px;
width: 2rem;
margin: .3rem 10px
}
.pagination__navigation .icon {
font-size: 2rem;
transition: .2s cubic-bezier(.4, 0, .6, 1);
vertical-align: middle
}
.pagination__navigation--disabled {
opacity: .6;
pointer-events: none
}
.pagination__more {
margin: .3rem;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: end;
-ms-flex-align: end;
align-items: flex-end;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
height: 2rem;
width: 2rem
}
.parallax {
position: relative;
overflow: hidden;
z-index: 0
}
.parallax__image-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
contain: strict
}
.parallax__image {
position: absolute;
bottom: 0;
left: 50%;
min-width: 100%;
min-height: 100%;
display: none;
-webkit-transform: translate(-50%);
transform: translate(-50%);
will-change: transform;
transition: opacity .3s cubic-bezier(.25, .8, .5, 1);
z-index: 1
}
.parallax__content {
color: #fff;
height: 100%;
z-index: 2;
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 0 1rem
}
.progress-circular {
position: relative;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex
}
.progress-circular--indeterminate svg {
-webkit-animation: g 1.4s linear infinite;
animation: g 1.4s linear infinite;
-webkit-transform-origin: center center;
transform-origin: center center;
width: 100%;
height: 100%;
margin: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
transition: all .2s ease-in-out;
z-index: 0
}
.progress-circular--indeterminate .progress-circular__overlay {
-webkit-animation: f 1.4s ease-in-out infinite;
animation: f 1.4s ease-in-out infinite;
stroke-linecap: round;
stroke-dasharray: 80, 200;
stroke-dashoffset: 0px
}
.progress-circular__underlay {
stroke: rgba(0, 0, 0, .1);
z-index: 1
}
.progress-circular__overlay {
stroke: currentColor;
z-index: 2;
transition: all .6s ease-in-out
}
.progress-circular__info {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%)
}
@-webkit-keyframes f {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0px
}
50% {
stroke-dasharray: 100, 200;
stroke-dashoffset: -15px
}
to {
stroke-dasharray: 100, 200;
stroke-dashoffset: -125px
}
}
@keyframes f {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0px
}
50% {
stroke-dasharray: 100, 200;
stroke-dashoffset: -15px
}
to {
stroke-dasharray: 100, 200;
stroke-dashoffset: -125px
}
}
@-webkit-keyframes g {
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@keyframes g {
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
.radio-group .input-group__details:after,
.radio-group .input-group__details:before {
display: none
}
.radio-group .input-group {
padding: 0
}
.radio-group--column .input-group__input {
display: block
}
.radio-group--row .input-group__input {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row
}
.radio-group.input-group--error .radio .icon--selection-control,
.radio-group.input-group--error .radio label {
color: inherit
}
.application .theme--light.input-group--slider label,
.theme--light .input-group--slider label {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.input-group--slider .slider__track,
.application .theme--light.input-group--slider .slider__track-fill,
.theme--light .input-group--slider .slider__track,
.theme--light .input-group--slider .slider__track-fill {
background: rgba(0, 0, 0, .26)
}
.application .theme--light.input-group--slider .slider__tick,
.application .theme--light.input-group--slider .slider__track__container:after,
.theme--light .input-group--slider .slider__tick,
.theme--light .input-group--slider .slider__track__container:after {
border: 1px solid rgba(0, 0, 0, .87)
}
.application .theme--light.input-group--slider:not(.input-group--dirty) .slider__thumb--label,
.theme--light .input-group--slider:not(.input-group--dirty) .slider__thumb--label {
background: rgba(0, 0, 0, .26)
}
.application .theme--light.input-group--slider:not(.input-group--dirty) .slider__thumb,
.theme--light .input-group--slider:not(.input-group--dirty) .slider__thumb {
border: 3px solid rgba(0, 0, 0, .26)
}
.application .theme--light.input-group--slider:not(.input-group--dirty):focus .slider__thumb,
.theme--light .input-group--slider:not(.input-group--dirty):focus .slider__thumb {
border: 3px solid rgba(0, 0, 0, .38)
}
.application .theme--light.input-group--slider.input-group--disabled .slider__thumb,
.theme--light .input-group--slider.input-group--disabled .slider__thumb {
background: none;
border: 3px solid rgba(0, 0, 0, .26)
}
.application .theme--light.input-group--slider.input-group--disabled.input-group--dirty .slider__thumb,
.theme--light .input-group--slider.input-group--disabled.input-group--dirty .slider__thumb {
background: rgba(0, 0, 0, .26);
border: 0 solid transparent
}
.application .theme--light.input-group--slider:focus .slider__track,
.theme--light .input-group--slider:focus .slider__track {
background: rgba(0, 0, 0, .38)
}
.application .theme--dark.input-group--slider label,
.theme--dark .input-group--slider label {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.input-group--slider .slider__track,
.application .theme--dark.input-group--slider .slider__track-fill,
.theme--dark .input-group--slider .slider__track,
.theme--dark .input-group--slider .slider__track-fill {
background: hsla(0, 0%, 100%, .2)
}
.application .theme--dark.input-group--slider .slider__tick,
.application .theme--dark.input-group--slider .slider__track__container:after,
.theme--dark .input-group--slider .slider__tick,
.theme--dark .input-group--slider .slider__track__container:after {
border: 1px solid #fff
}
.application .theme--dark.input-group--slider:not(.input-group--dirty) .slider__thumb--label,
.theme--dark .input-group--slider:not(.input-group--dirty) .slider__thumb--label {
background: hsla(0, 0%, 100%, .2)
}
.application .theme--dark.input-group--slider:not(.input-group--dirty) .slider__thumb,
.theme--dark .input-group--slider:not(.input-group--dirty) .slider__thumb {
border: 3px solid hsla(0, 0%, 100%, .2)
}
.application .theme--dark.input-group--slider:not(.input-group--dirty):focus .slider__thumb,
.theme--dark .input-group--slider:not(.input-group--dirty):focus .slider__thumb {
border: 3px solid hsla(0, 0%, 100%, .3)
}
.application .theme--dark.input-group--slider.input-group--disabled .slider__thumb,
.theme--dark .input-group--slider.input-group--disabled .slider__thumb {
background: none;
border: 3px solid hsla(0, 0%, 100%, .2)
}
.application .theme--dark.input-group--slider.input-group--disabled.input-group--dirty .slider__thumb,
.theme--dark .input-group--slider.input-group--disabled.input-group--dirty .slider__thumb {
background: hsla(0, 0%, 100%, .2);
border: 0 solid transparent
}
.application .theme--dark.input-group--slider:focus .slider__track,
.theme--dark .input-group--slider:focus .slider__track {
background: hsla(0, 0%, 100%, .3)
}
.input-group.input-group--slider {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
padding-right: 16px
}
.input-group.input-group--slider .input-group__details:after,
.input-group.input-group--slider .input-group__details:before {
display: none
}
.input-group.input-group--slider .input-group__input {
-webkit-box-flex: 1;
-ms-flex: 1 1 100%;
flex: 1 1 100%
}
.input-group.input-group--slider label {
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
width: auto;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 18px;
-webkit-transform: none;
transform: none
}
.input-group.input-group--slider label+.input-group__input {
margin-left: 16px;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto
}
.input-group.input-group--slider.input-group--active .slider__thumb {
-webkit-transform: translateY(-50%) scale(1.2);
transform: translateY(-50%) scale(1.2)
}
.input-group.input-group--slider.input-group--active .slider__track {
transition: none
}
.input-group.input-group--slider.input-group--active .slider__thumb-container--label .slider__thumb,
.input-group.input-group--slider.input-group--active .slider__thumb-container--label .slider__thumb:hover {
-webkit-transform: translateY(-50%) scale(0);
transform: translateY(-50%) scale(0)
}
.input-group.input-group--slider.input-group--active .slider__thumb-container,
.input-group.input-group--slider.input-group--active .slider__track-fill {
transition: none
}
.input-group.input-group--slider.input-group--active.input-group--ticks .slider__tick,
.input-group.input-group--slider.input-group--active.input-group--ticks .slider__track__container:after {
opacity: 1
}
.input-group.input-group--slider.input-group--disabled {
pointer-events: none
}
.input-group.input-group--slider.input-group--disabled .slider__thumb {
-webkit-transform: translateY(-50%) scale(.5);
transform: translateY(-50%) scale(.5);
background: transparent
}
.input-group.input-group--slider.input-group--disabled.input-group--dirty {
border-color: transparent
}
.input-group.input-group--slider.input-group--prepend-icon .slider {
margin-left: 40px
}
.input-group.input-group--slider.input-group--append-icon .slider {
margin-right: 40px
}
.slider {
cursor: default;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
height: 30px;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.slider__track__container {
position: absolute;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
height: 2px;
width: 100%;
overflow: hidden
}
.slider__track__container:after {
content: "";
position: absolute;
right: 0;
top: 0;
height: 2px;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 2px;
opacity: 0
}
.slider__thumb,
.slider__tick,
.slider__track {
position: absolute;
top: 0
}
.slider__track {
-webkit-transform-origin: right;
transform-origin: right;
overflow: hidden
}
.slider__track,
.slider__track-fill {
height: 2px;
left: 0;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%
}
.slider__track-fill {
position: absolute;
-webkit-transform-origin: left;
transform-origin: left
}
.slider__ticks-container {
position: absolute;
left: 0;
height: 2px;
width: 100%;
top: 50%;
overflow: hidden
}
.slider__tick {
transition: .3s cubic-bezier(.25, .8, .5, 1);
opacity: 0
}
.slider__thumb-container {
position: absolute
}
.slider__thumb,
.slider__thumb-container {
top: 50%;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.slider__thumb {
width: 16px;
height: 16px;
left: -8px;
border-radius: 50%;
background: transparent;
-webkit-transform: translateY(-50%) scale(.8);
transform: translateY(-50%) scale(.8);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.slider__thumb--label__container {
position: absolute;
left: 0;
top: 0;
transition: .3s ease-in-out
}
.slider__thumb--label {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
font-size: 12px;
color: #fff;
width: 28px;
height: 28px;
border-radius: 50% 50% 0;
position: absolute;
left: -14px;
top: -40px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
transition: .3s ease-in-out
}
.slider__thumb--label span {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg)
}
.slider__track,
.slider__track-fill {
position: absolute
}
.snack {
position: fixed;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: #fff;
pointer-events: none;
z-index: 1000;
font-size: 14px;
left: 0;
right: 0
}
.snack--absolute {
position: absolute
}
.snack--top {
top: 0
}
.snack--bottom {
bottom: 0
}
.snack__wrapper {
background-color: #323232;
pointer-events: auto;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12)
}
.snack__content,
.snack__wrapper {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 100%
}
.snack__content {
height: 48px;
padding: 14px 24px;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
overflow: hidden
}
.snack__content .btn {
color: #fff;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
margin: 0 0 0 24px;
height: auto;
min-width: auto;
width: auto
}
.snack__content .btn__content {
padding: 8px;
margin: -8px
}
.snack__content .btn__content:before {
display: none
}
.snack--multi-line .snack__content {
height: 80px;
padding: 24px
}
.snack--vertical .snack__content {
height: 112px;
padding: 24px 24px 14px;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch
}
.snack--vertical .snack__content .btn.btn {
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
margin-left: 0;
margin-top: 24px
}
.snack--vertical .snack__content .btn__content {
padding: 0;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0
}
.snack--auto-height .snack__content {
height: auto
}
.snack-transition-enter-active,
.snack-transition-leave-active {
transition: -webkit-transform .4s cubic-bezier(.25, .8, .5, 1);
transition: transform .4s cubic-bezier(.25, .8, .5, 1);
transition: transform .4s cubic-bezier(.25, .8, .5, 1), -webkit-transform .4s cubic-bezier(.25, .8, .5, 1)
}
.snack-transition-enter-active .snack__content,
.snack-transition-leave-active .snack__content {
transition: opacity .3s linear .1s
}
.snack-transition-enter .snack__content {
opacity: 0
}
.snack-transition-enter-to .snack__content,
.snack-transition-leave .snack__content {
opacity: 1
}
.snack-transition-enter.snack.snack--top,
.snack-transition-leave-to.snack.snack--top {
-webkit-transform: translateY(calc(-100% - 8px));
transform: translateY(calc(-100% - 8px))
}
.snack-transition-enter.snack.snack--bottom,
.snack-transition-leave-to.snack.snack--bottom {
-webkit-transform: translateY(100%);
transform: translateY(100%)
}
@media only screen and (min-width:600px) {
.snack__wrapper {
width: auto;
max-width: 568px;
min-width: 288px;
margin: 0 auto;
border-radius: 2px
}
.snack--left .snack__wrapper {
margin-left: 0
}
.snack--right .snack__wrapper {
margin-right: 0
}
.snack--left,
.snack--right {
margin: 0 24px
}
.snack--left.snack--top,
.snack--right.snack--top {
-webkit-transform: translateY(24px);
transform: translateY(24px)
}
.snack--left.snack--bottom,
.snack--right.snack--bottom {
-webkit-transform: translateY(-24px);
transform: translateY(-24px)
}
.snack__content .btn:first-of-type {
margin-left: 48px
}
}
.speed-dial {
position: relative
}
.speed-dial--absolute {
position: absolute
}
.speed-dial--fixed {
position: fixed
}
.speed-dial--top:not(.speed-dial--absolute) {
top: 16px
}
.speed-dial--top.speed-dial--absolute {
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%)
}
.speed-dial--bottom:not(.speed-dial--absolute) {
bottom: 16px
}
.speed-dial--bottom.speed-dial--absolute {
bottom: 50%;
-webkit-transform: translateY(50%);
transform: translateY(50%)
}
.speed-dial--left {
left: 16px
}
.speed-dial--right {
right: 16px
}
.speed-dial--direction-left .speed-dial__list,
.speed-dial--direction-right .speed-dial__list {
height: 100%;
top: 0
}
.speed-dial--direction-bottom .speed-dial__list,
.speed-dial--direction-top .speed-dial__list {
left: 0;
width: 100%
}
.speed-dial--direction-top .speed-dial__list {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse;
bottom: 100%
}
.speed-dial--direction-right .speed-dial__list {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
left: 100%
}
.speed-dial--direction-bottom .speed-dial__list {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
top: 100%
}
.speed-dial--direction-left .speed-dial__list {
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-ms-flex-direction: row-reverse;
flex-direction: row-reverse;
right: 100%
}
.speed-dial__list {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: absolute
}
.speed-dial__list .btn:first-child {
transition-delay: .05s
}
.speed-dial__list .btn:nth-child(2) {
transition-delay: .1s
}
.speed-dial__list .btn:nth-child(3) {
transition-delay: .15s
}
.speed-dial__list .btn:nth-child(4) {
transition-delay: .2s
}
.speed-dial__list .btn:nth-child(5) {
transition-delay: .25s
}
.speed-dial__list .btn:nth-child(6) {
transition-delay: .3s
}
.speed-dial__list .btn:nth-child(7) {
transition-delay: .35s
}
.application .theme--light.stepper,
.theme--light .stepper {
background: #fff
}
.application .theme--light.stepper .stepper__step:not(.stepper__step--active):not(.stepper__step--complete):not(.stepper__step--error) .stepper__step__step,
.theme--light .stepper .stepper__step:not(.stepper__step--active):not(.stepper__step--complete):not(.stepper__step--error) .stepper__step__step {
background: rgba(0, 0, 0, .38)
}
.application .theme--light.stepper .stepper__step__step,
.application .theme--light.stepper .stepper__step__step .icon,
.theme--light .stepper .stepper__step__step,
.theme--light .stepper .stepper__step__step .icon {
color: #fff
}
.application .theme--light.stepper .stepper__header .divider,
.theme--light .stepper .stepper__header .divider {
background: rgba(0, 0, 0, .12)
}
.application .theme--light.stepper .stepper__step--active .stepper__label,
.theme--light .stepper .stepper__step--active .stepper__label {
text-shadow: 0 0 0 #000
}
.application .theme--light.stepper .stepper__step--editable:hover,
.theme--light .stepper .stepper__step--editable:hover {
background: rgba(0, 0, 0, .06)
}
.application .theme--light.stepper .stepper__step--editable:hover .stepper__label,
.theme--light .stepper .stepper__step--editable:hover .stepper__label {
text-shadow: 0 0 0 #000
}
.application .theme--light.stepper .stepper__step--complete .stepper__label,
.theme--light .stepper .stepper__step--complete .stepper__label {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.stepper .stepper__step--inactive.stepper__step--editable:not(.stepper__step--error):hover .stepper__step__step,
.theme--light .stepper .stepper__step--inactive.stepper__step--editable:not(.stepper__step--error):hover .stepper__step__step {
background: rgba(0, 0, 0, .54)
}
.application .theme--light.stepper .stepper__label,
.theme--light .stepper .stepper__label {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.stepper--non-linear .stepper__step:not(.stepper__step--complete):not(.stepper__step--error) .stepper__label,
.application .theme--light.stepper .stepper__label small,
.theme--light .stepper--non-linear .stepper__step:not(.stepper__step--complete):not(.stepper__step--error) .stepper__label,
.theme--light .stepper .stepper__label small {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.stepper--vertical .stepper__content:not(:last-child),
.theme--light .stepper--vertical .stepper__content:not(:last-child) {
border-left: 1px solid rgba(0, 0, 0, .12)
}
.application .theme--dark.stepper,
.theme--dark .stepper {
background: #303030
}
.application .theme--dark.stepper .stepper__step:not(.stepper__step--active):not(.stepper__step--complete):not(.stepper__step--error) .stepper__step__step,
.theme--dark .stepper .stepper__step:not(.stepper__step--active):not(.stepper__step--complete):not(.stepper__step--error) .stepper__step__step {
background: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.stepper .stepper__step__step,
.application .theme--dark.stepper .stepper__step__step .icon,
.theme--dark .stepper .stepper__step__step,
.theme--dark .stepper .stepper__step__step .icon {
color: #fff
}
.application .theme--dark.stepper .stepper__header .divider,
.theme--dark .stepper .stepper__header .divider {
background: hsla(0, 0%, 100%, .12)
}
.application .theme--dark.stepper .stepper__step--active .stepper__label,
.theme--dark .stepper .stepper__step--active .stepper__label {
text-shadow: 0 0 0 #fff
}
.application .theme--dark.stepper .stepper__step--editable:hover,
.theme--dark .stepper .stepper__step--editable:hover {
background: hsla(0, 0%, 100%, .06)
}
.application .theme--dark.stepper .stepper__step--editable:hover .stepper__label,
.theme--dark .stepper .stepper__step--editable:hover .stepper__label {
text-shadow: 0 0 0 #fff
}
.application .theme--dark.stepper .stepper__step--complete .stepper__label,
.theme--dark .stepper .stepper__step--complete .stepper__label {
color: hsla(0, 0%, 100%, .87)
}
.application .theme--dark.stepper .stepper__step--inactive.stepper__step--editable:not(.stepper__step--error):hover .stepper__step__step,
.theme--dark .stepper .stepper__step--inactive.stepper__step--editable:not(.stepper__step--error):hover .stepper__step__step {
background: hsla(0, 0%, 100%, .75)
}
.application .theme--dark.stepper .stepper__label,
.theme--dark .stepper .stepper__label {
color: hsla(0, 0%, 100%, .5)
}
.application .theme--dark.stepper--non-linear .stepper__step:not(.stepper__step--complete):not(.stepper__step--error) .stepper__label,
.application .theme--dark.stepper .stepper__label small,
.theme--dark .stepper--non-linear .stepper__step:not(.stepper__step--complete):not(.stepper__step--error) .stepper__label,
.theme--dark .stepper .stepper__label small {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.stepper--vertical .stepper__content:not(:last-child),
.theme--dark .stepper--vertical .stepper__content:not(:last-child) {
border-left: 1px solid hsla(0, 0%, 100%, .12)
}
.stepper {
overflow: hidden;
position: relative
}
.stepper,
.stepper__header {
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.stepper__header {
height: 72px;
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between
}
.stepper__header .divider {
-ms-flex-item-align: center;
align-self: center;
margin: 0 -16px
}
.stepper__items {
position: relative;
overflow: hidden
}
.stepper__step__step {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: 50%;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
font-size: 12px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
height: 24px;
margin-right: 8px;
min-width: 24px;
width: 24px;
transition: .3s cubic-bezier(.25, .8, .25, 1)
}
.stepper__step__step .icon {
font-size: 18px
}
.stepper__step {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
padding: 24px;
position: relative
}
.stepper__step--active .stepper__label {
transition: .3s cubic-bezier(.4, 0, .6, 1)
}
.stepper__step--editable {
cursor: pointer
}
.stepper__step.stepper__step--error .stepper__step__step {
background: transparent;
color: inherit
}
.stepper__step.stepper__step--error .stepper__step__step .icon {
font-size: 24px;
color: inherit
}
.stepper__step.stepper__step--error .stepper__label {
color: inherit;
text-shadow: none;
font-weight: 500
}
.stepper__step.stepper__step--error .stepper__label small {
color: inherit
}
.stepper__label {
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
text-align: left
}
.stepper__label small {
font-size: 12px;
font-weight: 300;
text-shadow: none
}
.stepper__wrapper {
overflow: hidden;
transition: none
}
.stepper__content {
top: 0;
padding: 24px 24px 16px;
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
width: 100%
}
.stepper__content>.btn {
margin: 24px 8px 8px 0
}
.stepper--is-booted .stepper__content,
.stepper--is-booted .stepper__wrapper {
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.stepper--vertical {
padding-bottom: 36px
}
.stepper--vertical .stepper__content {
margin: -8px -36px -16px 36px;
padding: 16px 60px 16px 23px;
width: auto
}
.stepper--vertical .stepper__step {
padding: 24px 24px 16px
}
.stepper--vertical .stepper__step__step {
margin-right: 12px
}
.stepper--alt-labels .stepper__header {
height: auto
}
.stepper--alt-labels .stepper__header .divider {
margin: 35px -67px 0;
-ms-flex-item-align: start;
align-self: flex-start
}
.stepper--alt-labels .stepper__step {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-preferred-size: 175px;
flex-basis: 175px
}
.stepper--alt-labels .stepper__step small {
-ms-flex-item-align: center;
align-self: center
}
.stepper--alt-labels .stepper__step__step {
margin-right: 0;
margin-bottom: 11px
}
@media only screen and (max-width:959px) {
.stepper:not(.stepper--vertical) .stepper__label {
display: none
}
.stepper:not(.stepper--vertical) .stepper__step__step {
margin-right: 0
}
}
.application .theme--light.subheader,
.theme--light .subheader {
color: rgba(0, 0, 0, .54)
}
.application .theme--dark.subheader,
.theme--dark .subheader {
color: hsla(0, 0%, 100%, .7)
}
.subheader {
height: 48px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 14px;
font-weight: 500;
padding: 0 16px
}
.subheader--inset {
margin-left: 56px
}
.application .theme--light.switch:not(.input-group--dirty) .input-group--selection-controls__container,
.theme--light .switch:not(.input-group--dirty) .input-group--selection-controls__container {
color: rgba(0, 0, 0, .38) !important
}
.application .theme--light.switch .input-group--selection-controls__ripple:after,
.theme--light .switch .input-group--selection-controls__ripple:after {
background-color: #fafafa
}
.application .theme--light.switch .input-group--selection-controls__ripple:not(.input-group--selection-controls__ripple--active),
.theme--light .switch .input-group--selection-controls__ripple:not(.input-group--selection-controls__ripple--active) {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.switch .input-group--selection-controls__ripple--active:after,
.theme--light .switch .input-group--selection-controls__ripple--active:after {
background-color: currentColor
}
.application .theme--light.switch .input-group--selection-controls__toggle,
.theme--light .switch .input-group--selection-controls__toggle {
color: rgba(0, 0, 0, .38)
}
.application .theme--light.switch .input-group--selection-controls__toggle--active,
.theme--light .switch .input-group--selection-controls__toggle--active {
color: inherit
}
.application .theme--light.switch.input-group--disabled .input-group--selection-controls__ripple:after,
.theme--light .switch.input-group--disabled .input-group--selection-controls__ripple:after {
background-color: #bdbdbd !important
}
.application .theme--light.switch.input-group--disabled .input-group--selection-controls__toggle,
.theme--light .switch.input-group--disabled .input-group--selection-controls__toggle {
color: rgba(0, 0, 0, .12) !important
}
.application .theme--dark.switch:not(.input-group--dirty) .input-group--selection-controls__container,
.theme--dark .switch:not(.input-group--dirty) .input-group--selection-controls__container {
color: hsla(0, 0%, 100%, .3) !important
}
.application .theme--dark.switch .input-group--selection-controls__ripple:after,
.theme--dark .switch .input-group--selection-controls__ripple:after {
background-color: #bdbdbd
}
.application .theme--dark.switch .input-group--selection-controls__ripple:not(.input-group--selection-controls__ripple--active),
.theme--dark .switch .input-group--selection-controls__ripple:not(.input-group--selection-controls__ripple--active) {
color: hsla(0, 0%, 100%, .3)
}
.application .theme--dark.switch .input-group--selection-controls__ripple--active:after,
.theme--dark .switch .input-group--selection-controls__ripple--active:after {
background-color: currentColor
}
.application .theme--dark.switch .input-group--selection-controls__toggle,
.theme--dark .switch .input-group--selection-controls__toggle {
color: hsla(0, 0%, 100%, .3)
}
.application .theme--dark.switch .input-group--selection-controls__toggle--active,
.theme--dark .switch .input-group--selection-controls__toggle--active {
color: inherit
}
.application .theme--dark.switch.input-group--disabled .input-group--selection-controls__ripple:after,
.theme--dark .switch.input-group--disabled .input-group--selection-controls__ripple:after {
background-color: #424242 !important
}
.application .theme--dark.switch.input-group--disabled .input-group--selection-controls__toggle,
.theme--dark .switch.input-group--disabled .input-group--selection-controls__toggle {
color: hsla(0, 0%, 100%, .1) !important
}
.input-group.input-group--selection-controls {
z-index: 0
}
.input-group.input-group--selection-controls.switch.input-group--append-icon label,
.input-group.input-group--selection-controls.switch.input-group--prepend-icon label {
left: 62px
}
.input-group.input-group--selection-controls.switch.input-group--prepend-icon .input-group--selection-controls__container {
margin-left: 6px
}
.input-group.input-group--selection-controls.switch.input-group--append-icon .input-group__append-icon {
left: 40px
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__container {
color: inherit;
position: relative;
width: 36px
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__container[class*="--text"] .input-group--selection-controls__ripple--active:after {
background-color: currentColor
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__toggle {
background-color: currentColor;
color: inherit;
position: absolute;
height: 14px;
top: 50%;
left: 0;
width: 34px;
border-radius: 8px;
-webkit-transform: translateY(-50%);
transform: translateY(-50%)
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__toggle.input-group--selection-controls__toggle--active {
opacity: .5
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__ripple {
-webkit-transform: translate(-15px, -24px);
transform: translate(-15px, -24px);
transition: .3s cubic-bezier(.25, .8, .25, 1);
z-index: 1;
left: 0
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__ripple:after {
content: "";
position: absolute;
display: inline-block;
cursor: pointer;
width: 20px;
border-radius: 50%;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
height: 20px;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, .2), 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12)
}
.input-group.input-group--selection-controls.switch .input-group--selection-controls__ripple--active {
-webkit-transform: translate(2px, -24px);
transform: translate(2px, -24px)
}
.input-group.input-group--selection-controls.switch label {
padding-left: 14px
}
.application .theme--light.system-bar,
.theme--light .system-bar {
background-color: #e0e0e0;
color: rgba(0, 0, 0, .54)
}
.application .theme--light.system-bar .icon,
.theme--light .system-bar .icon {
color: rgba(0, 0, 0, .54)
}
.application .theme--light.system-bar--lights-out,
.theme--light .system-bar--lights-out {
background-color: hsla(0, 0%, 100%, .7) !important
}
.application .theme--dark.system-bar,
.theme--dark .system-bar {
background-color: #000;
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.system-bar .icon,
.theme--dark .system-bar .icon {
color: hsla(0, 0%, 100%, .7)
}
.application .theme--dark.system-bar--lights-out,
.theme--dark .system-bar--lights-out {
background-color: rgba(0, 0, 0, .2) !important
}
.system-bar {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 14px;
font-weight: 500;
padding: 0 8px
}
.system-bar .icon {
font-size: 16px
}
.system-bar--absolute,
.system-bar--fixed {
left: 0;
top: 0;
width: 100%;
z-index: 3
}
.system-bar--fixed {
position: fixed
}
.system-bar--absolute {
position: absolute
}
.system-bar--status .icon {
margin-right: 4px
}
.system-bar--window .icon {
font-size: 20px;
margin-right: 8px
}
.application .theme--light.tabs__bar,
.theme--light .tabs__bar {
background-color: #fff
}
.application .theme--light.tabs__bar .tabs__div,
.theme--light .tabs__bar .tabs__div {
color: rgba(0, 0, 0, .87)
}
.application .theme--light.tabs__bar .tabs__div.tabs__item--disabled,
.theme--light .tabs__bar .tabs__div.tabs__item--disabled {
color: rgba(0, 0, 0, .26)
}
.application .theme--dark.tabs__bar,
.theme--dark .tabs__bar {
background-color: #424242
}
.application .theme--dark.tabs__bar .tabs__div,
.theme--dark .tabs__bar .tabs__div {
color: #fff
}
.application .theme--dark.tabs__bar .tabs__div.tabs__item--disabled,
.theme--dark .tabs__bar .tabs__div.tabs__item--disabled {
color: hsla(0, 0%, 100%, .3)
}
.tabs,
.tabs__bar {
position: relative
}
.tabs__icon {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
height: 100%;
position: absolute;
top: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 32px
}
.tabs__icon--prev {
left: 4px
}
.tabs__icon--next {
right: 4px
}
.tabs__wrapper {
overflow: hidden;
contain: content;
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.tabs__wrapper--show-arrows {
margin-left: 40px;
margin-right: 40px
}
.tabs__wrapper--show-arrows .tabs__container--align-with-title {
padding-left: 16px
}
.tabs__container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 48px;
list-style-type: none;
transition: -webkit-transform .6s cubic-bezier(.86, 0, .07, 1);
transition: transform .6s cubic-bezier(.86, 0, .07, 1);
transition: transform .6s cubic-bezier(.86, 0, .07, 1), -webkit-transform .6s cubic-bezier(.86, 0, .07, 1);
white-space: nowrap;
position: relative
}
.tabs__container,
.tabs__container--grow .tabs__div,
.tabs__container--overflow .tabs__div {
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto
}
.tabs__container--grow .tabs__div {
max-width: none
}
.tabs__container--icons-and-text {
height: 72px
}
.tabs__container--align-with-title {
padding-left: 56px
}
.tabs__container--centered .tabs__div,
.tabs__container--fixed-tabs .tabs__div,
.tabs__container--icons-and-text .tabs__div {
min-width: 72px
}
.tabs__container--centered .tabs__slider-wrapper+.tabs__div,
.tabs__container--centered>.tabs__div:first-child,
.tabs__container--fixed-tabs .tabs__slider-wrapper+.tabs__div,
.tabs__container--fixed-tabs>.tabs__div:first-child,
.tabs__container--right .tabs__slider-wrapper+.tabs__div,
.tabs__container--right>.tabs__div:first-child {
margin-left: auto
}
.tabs__container--centered>.tabs__div:last-child,
.tabs__container--fixed-tabs>.tabs__div:last-child {
margin-right: auto
}
.tabs__container--icons-and-text .tabs__item {
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-ms-flex-direction: column-reverse;
flex-direction: column-reverse
}
.tabs__container--icons-and-text .tabs__item .icon {
margin-bottom: 6px
}
.tabs__div {
-ms-flex-align: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
font-size: 14px;
font-weight: 500;
line-height: normal;
height: inherit;
max-width: 264px;
text-align: center;
text-transform: uppercase;
vertical-align: middle
}
.tabs__div,
.tabs__item {
-webkit-box-align: center;
align-items: center
}
.tabs__item {
-ms-flex-align: center;
color: inherit;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1 1;
flex: 1 1;
-ms-flex-preferred-size: 264px;
flex-basis: 264px;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
max-width: inherit;
padding: 6px 12px;
text-decoration: none;
transition: .3s cubic-bezier(.25, .8, .5, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: normal
}
.tabs__item:not(.tabs__item--active) {
opacity: .7
}
.tabs__slider {
height: 2px;
width: 100%
}
.tabs__slider-wrapper {
bottom: 0;
margin: 0 !important;
position: absolute;
transition: .3s cubic-bezier(.25, .8, .5, 1)
}
.tabs__items {
overflow: hidden;
position: relative
}
.tabs__content {
width: 100%;
transition: -webkit-transform .4s cubic-bezier(.86, 0, .07, 1);
transition: transform .4s cubic-bezier(.86, 0, .07, 1);
transition: transform .4s cubic-bezier(.86, 0, .07, 1), -webkit-transform .4s cubic-bezier(.86, 0, .07, 1)
}
@media only screen and (max-width:599px) {
.tabs__wrapper--show-arrows .tabs__container--align-with-title {
padding-left: 24px
}
.tabs__container--align-with-title {
padding-left: 64px
}
.tabs__container--fixed-tabs .tabs__div {
-webkit-box-flex: 1;
-ms-flex: 1 0 auto;
flex: 1 0 auto
}
}
@media only screen and (min-width:600px) {
.tabs__container--centered .tabs__div,
.tabs__container--fixed-tabs .tabs__div,
.tabs__container--icons-and-text .tabs__div {
min-width: 160px
}
}
.time-picker-title {
color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
line-height: 1;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end
}
.time-picker-title__time {
white-space: nowrap
}
.time-picker-title__time .picker__title__btn,
.time-picker-title__time span {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
height: 70px;
font-size: 70px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center
}
.time-picker-title__ampm {
-ms-flex-item-align: end;
align-self: flex-end;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
font-size: 16px;
margin: 8px 0 6px 8px;
text-transform: uppercase
}
.time-picker-title__ampm div:only-child {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row
}
.picker__title--landscape .time-picker-title {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
height: 100%
}
.picker__title--landscape .time-picker-title__time {
text-align: right
}
.picker__title--landscape .time-picker-title__time .picker__title__btn,
.picker__title--landscape .time-picker-title__time span {
height: 55px;
font-size: 55px
}
.picker__title--landscape .time-picker-title__ampm {
margin: 16px 0 0;
-ms-flex-item-align: initial;
align-self: auto;
text-align: center
}
.application .theme--light.time-picker-clock,
.theme--light .time-picker-clock {
background: #e0e0e0
}
.application .theme--light.time-picker-clock>span.disabled,
.theme--light .time-picker-clock>span.disabled {
color: rgba(0, 0, 0, .26)
}
.application .theme--light.time-picker-clock>span.disabled.active,
.theme--light .time-picker-clock>span.disabled.active {
color: hsla(0, 0%, 100%, .3)
}
.application .theme--light.time-picker-clock--indeterminate .time-picker-clock__hand,
.theme--light .time-picker-clock--indeterminate .time-picker-clock__hand {
background-color: #bdbdbd
}
.application .theme--light.time-picker-clock--indeterminate .time-picker-clock__hand:after,
.theme--light .time-picker-clock--indeterminate .time-picker-clock__hand:after {
color: #bdbdbd
}
.application .theme--light.time-picker-clock--indeterminate>span.active,
.theme--light .time-picker-clock--indeterminate>span.active {
background-color: #bdbdbd
}
.application .theme--dark.time-picker-clock,
.theme--dark .time-picker-clock {
background: #616161
}
.application .theme--dark.time-picker-clock>span.disabled,
.application .theme--dark.time-picker-clock>span.disabled.active,
.theme--dark .time-picker-clock>span.disabled,
.theme--dark .time-picker-clock>span.disabled.active {
color: hsla(0, 0%, 100%, .3)
}
.application .theme--dark.time-picker-clock--indeterminate .time-picker-clock__hand,
.theme--dark .time-picker-clock--indeterminate .time-picker-clock__hand {
background-color: #757575
}
.application .theme--dark.time-picker-clock--indeterminate .time-picker-clock__hand:after,
.theme--dark .time-picker-clock--indeterminate .time-picker-clock__hand:after {
color: #757575
}
.application .theme--dark.time-picker-clock--indeterminate>span.active,
.theme--dark .time-picker-clock--indeterminate>span.active {
background-color: #757575
}
.time-picker-clock {
border-radius: 100%;
position: relative;
transition: .3s cubic-bezier(.25, .8, .5, 1);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.time-picker-clock__container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 10px
}
.time-picker-clock__hand {
height: calc(50% - 28px);
width: 2px;
bottom: 50%;
left: calc(50% - 1px);
-webkit-transform-origin: center bottom;
transform-origin: center bottom;
position: absolute;
will-change: transform;
z-index: 1
}
.time-picker-clock__hand:before {
background: transparent;
border-width: 2px;
width: 10px;
height: 10px;
top: -3%
}
.time-picker-clock__hand:after,
.time-picker-clock__hand:before {
border-style: solid;
border-color: inherit;
border-radius: 100%;
content: "";
position: absolute;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%)
}
.time-picker-clock__hand:after {
height: 8px;
width: 8px;
top: 100%;
background-color: inherit
}
.time-picker-clock>span {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: 100%;
cursor: default;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 16px;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
left: calc(50% - 40px / 2);
height: 40px;
position: absolute;
text-align: center;
top: calc(50% - 40px / 2);
width: 40px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
.time-picker-clock>span>span {
z-index: 1
}
.time-picker-clock>span:after,
.time-picker-clock>span:before {
content: "";
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
height: 14px;
width: 14px;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
height: 40px;
width: 40px
}
.time-picker-clock>span.active {
color: #fff;
cursor: default;
z-index: 2
}
.time-picker-clock>span.disabled {
pointer-events: none
}
.application .theme--light.toolbar,
.theme--light .toolbar {
background-color: #f5f5f5;
color: rgba(0, 0, 0, .87)
}
.application .theme--dark.toolbar,
.theme--dark .toolbar {
background-color: #212121;
color: #fff
}
.toolbar {
transition: none;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, .2), 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12);
display: block;
position: relative;
width: 100%;
will-change: padding-left
}
.toolbar[data-booted=true] {
transition: .2s cubic-bezier(.4, 0, .2, 1)
}
.toolbar .input-group--solo .input-group__details {
display: none
}
.toolbar .input-group--single-line:not(.input-group--solo) {
padding: 0
}
.toolbar .input-group--single-line:not(.input-group--solo) label {
top: auto
}
.toolbar .tabs {
width: 100%
}
.toolbar__title {
font-size: 20px;
font-weight: 500;
letter-spacing: .02em;
margin-left: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
}
.toolbar__content,
.toolbar__extension {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.toolbar__content>.list,
.toolbar__extension>.list {
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin: 0 !important;
max-height: 100%
}
.toolbar__content>.btn:last-child,
.toolbar__content>.menu:first-child,
.toolbar__extension>.btn:last-child,
.toolbar__extension>.menu:first-child {
margin-right: 8px
}
.toolbar__content>.btn:first-child,
.toolbar__content>.menu:first-child,
.toolbar__extension>.btn:first-child,
.toolbar__extension>.menu:first-child {
margin-left: 8px
}
.toolbar__content>:not(.btn):not(.menu):first-child:not(:only-child),
.toolbar__extension>:not(.btn):not(.menu):first-child:not(:only-child) {
margin-left: 16px
}
.toolbar__content>:not(.btn):not(.menu):last-child:not(:only-child),
.toolbar__extension>:not(.btn):not(.menu):last-child:not(:only-child) {
margin-right: 16px
}
.toolbar__items {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: inherit;
max-width: 100%;
padding: 0
}
.toolbar__items .btn {
-webkit-box-align: stretch;
-ms-flex-align: stretch;
align-items: stretch
}
.toolbar__items .tooltip,
.toolbar__items .tooltip>span {
height: inherit
}
.toolbar__items .btn,
.toolbar__items .menu,
.toolbar__items .menu__activator {
height: inherit;
margin: 0
}
.toolbar--card {
border-radius: 2px 2px 0 0;
box-shadow: 0 0 0 0 rgba(0, 0, 0, .2), 0 0 0 0 rgba(0, 0, 0, .14), 0 0 0 0 rgba(0, 0, 0, .12)
}
.toolbar--fixed {
position: fixed;
z-index: 2
}
.toolbar--absolute,
.toolbar--fixed {
top: 0;
left: 0
}
.toolbar--absolute {
position: absolute;
z-index: 2
}
.toolbar--floating {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
margin: 16px;
width: auto
}
.toolbar--clipped {
z-index: 3
}
@media only screen and (max-width:599px) {
.toolbar .toolbar__content>.btn:last-child,
.toolbar .toolbar__extension>.btn:last-child {
margin-right: 17px
}
.toolbar .toolbar__content>.btn:first-child,
.toolbar .toolbar__extension>.btn:first-child {
margin-left: 17px
}
.toolbar .toolbar__content>:not(.btn):not(.menu):first-child:not(:only-child),
.toolbar .toolbar__extension>:not(.btn):not(.menu):first-child:not(:only-child) {
margin-left: 24px
}
.toolbar .toolbar__content>:not(.btn):not(.menu):last-child:not(:only-child),
.toolbar .toolbar__extension>:not(.btn):not(.menu):last-child:not(:only-child) {
margin-right: 24px
}
}
.tooltip {
position: relative
}
.tooltip__content {
background: #616161;
border-radius: 2px;
color: #fff;
font-size: 12px;
display: inline-block;
padding: 5px 8px;
position: absolute;
text-transform: none;
transition: .15s cubic-bezier(.25, .8, .5, 1);
width: auto;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12)
}
.tooltip__content[class*=-active] {
pointer-events: none
}
@media only screen and (max-width:959px) {
.tooltip .tooltip__content {
padding: 10px 16px
}
}
/*# sourceMappingURL=vuetify.min.css.map*/