Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production deploy #4167

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions editor.planx.uk/src/@planx/components/List/Public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import { isMapFieldResponse } from "@planx/components/shared/Schema/model";
import { SchemaFields } from "@planx/components/shared/Schema/SchemaFields";
import { PublicProps } from "@planx/components/shared/types";
import React, { useEffect, useRef } from "react";
Expand Down Expand Up @@ -128,21 +129,26 @@ const InactiveListCard: React.FC<{
}> = ({ index: i }) => {
const { schema, formik, removeItem, editItem } = useListContext();

const mapPreview = schema.fields.find((field) => field.type === "map");
const formattedMapResponse = (() => {
const mapField = schema.fields.find((field) => field.type === "map");
if (!mapField) return null;

const mapResponse = formik.values.schemaData[i][mapField.data.fn];
if (!mapResponse) return null;

return isMapFieldResponse(mapResponse)
? formatSchemaDisplayValue(mapResponse, mapField)
: null;
})();

return (
<ListCard data-testid={`list-card-${i}`}>
<Typography component="h2" variant="h3">
{`${schema.type} ${i + 1}`}
</Typography>
<InactiveListCardLayout>
{mapPreview && (
<Box sx={{ flexBasis: "50%" }}>
{formatSchemaDisplayValue(
formik.values.schemaData[i][mapPreview.data.fn],
mapPreview,
)}
</Box>
{formattedMapResponse && (
<Box sx={{ flexBasis: "50%" }}>{formattedMapResponse}</Box>
)}
<Table>
<TableBody>
Expand Down
3 changes: 1 addition & 2 deletions editor.planx.uk/src/@planx/components/List/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SchemaUserResponse } from "./../shared/Schema/model";
import {
flatten,
sumIdenticalUnits,
Expand Down Expand Up @@ -63,7 +62,7 @@ describe("passport data shape", () => {
identicalUnits: 2,
},
],
} as unknown as Record<string, SchemaUserResponse[]>;
};

expect(
sumIdenticalUnits("proposal.units.residential", defaultPassportData),
Expand Down
62 changes: 49 additions & 13 deletions editor.planx.uk/src/@planx/components/List/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { styled } from "@mui/material/styles";
import React from "react";

import { Field, SchemaUserResponse } from "./../shared/Schema/model";
import {
Field,
isChecklistFieldResponse,
isMapFieldResponse,
isNumberFieldResponse,
isTextResponse,
NumberField,
ResponseValue,
SchemaUserResponse,
} from "./../shared/Schema/model";

const List = styled("ul")(() => ({
listStylePosition: "inside",
Expand All @@ -15,19 +24,27 @@ const List = styled("ul")(() => ({
* @param field - the Field object
* @returns string | React.JSX.Element - the `text` for the given value `val`, or the original value
*/
export function formatSchemaDisplayValue(
value: string | string[],
field: Field,
) {
export const formatSchemaDisplayValue = <T extends Field>(
value: ResponseValue<T>,
field: T,
) => {
switch (field.type) {
case "number":
case "number": {
if (!isNumberFieldResponse(value)) return;

return field.data.units ? `${value} ${field.data.units}` : value;
}
case "text":
case "date":
case "date": {
if (!isTextResponse(value)) return;

return value;
}
case "checklist": {
if (!isChecklistFieldResponse(value)) return;

const matchingOptions = field.data.options.filter((option) =>
(value as string[]).includes(option.id),
value.includes(option.id),
);
return (
<List>
Expand All @@ -38,12 +55,16 @@ export function formatSchemaDisplayValue(
);
}
case "question": {
if (!isTextResponse(value)) return;

const matchingOption = field.data.options.find(
(option) => option.data.text === value || option.data.val === value,
);
return matchingOption?.data.text;
}
case "map": {
if (!isMapFieldResponse(value)) return;

return (
<>
{/* @ts-ignore */}
Expand Down Expand Up @@ -75,7 +96,22 @@ export function formatSchemaDisplayValue(
);
}
}
}
};

const isIdenticalUnitsField = (
item: SchemaUserResponse,
): item is Record<"identicalUnits", ResponseValue<NumberField>> =>
"identicalUnits" in item && isNumberFieldResponse(item.identicalUnits);

const isIdenticalUnitsDevelopmentField = (
item: SchemaUserResponse,
): item is Record<
"identicalUnits" | "development",
ResponseValue<NumberField>
> =>
"identicalUnits" in item &&
"development" in item &&
isNumberFieldResponse(item.identicalUnits);

/**
* If the schema includes a field that sets fn = "identicalUnits", sum of total units
Expand All @@ -89,8 +125,8 @@ export function sumIdenticalUnits(
): number {
let sum = 0;
passportData[`${fn}`].map((item) => {
if (!Array.isArray(item?.identicalUnits)) {
sum += parseInt(item?.identicalUnits);
if (isIdenticalUnitsField(item)) {
sum += item.identicalUnits;
}
});
return sum;
Expand Down Expand Up @@ -119,8 +155,8 @@ export function sumIdenticalUnitsByDevelopmentType(
notKnown: 0,
};
passportData[`${fn}`].map((item) => {
if (!Array.isArray(item?.identicalUnits)) {
baseSums[`${item?.development}`] += parseInt(item?.identicalUnits);
if (isIdenticalUnitsDevelopmentField(item)) {
baseSums[`${item?.development}`] += item.identicalUnits;
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ import { Store, useStore } from "pages/FlowEditor/lib/store";
import React, { useState } from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";

import { SchemaUserResponse } from "../Schema/model";
import {
Field,
isMapFieldResponse,
isNumberFieldResponse,
isTextResponse,
ResponseValue,
SchemaUserResponse,
} from "../Schema/model";

export default SummaryListsBySections;

Expand Down Expand Up @@ -626,6 +633,15 @@ function Page(props: ComponentProps) {
const answers = getAnswersByNode(props) as SchemaUserResponse[];
const fields = (props.node.data as Page).schema.fields;

const displayValue = (answer: ResponseValue<Field>) => {
if (isTextResponse(answer)) return answer;
if (isNumberFieldResponse(answer)) return answer.toString();
if (isMapFieldResponse(answer)) return `${answer.length || 0} features`;

// TODO: Handle other types more gracefully
return answer;
};

return (
<>
<Box component="dt">{props.node.data.title}</Box>
Expand All @@ -639,9 +655,7 @@ function Page(props: ComponentProps) {
{field.data.title}
</Typography>
<Typography>
{field.type === "map"
? `${answers[0][field.data.fn].length || 0} features`
: answers[0][field.data.fn]}
<>{displayValue(answers[0][field.data.fn])}</>
</Typography>
</Box>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
Field,
ResponseValue,
SchemaUserData,
} from "@planx/components/shared/Schema/model";
import { FormikProps } from "formik";
Expand Down Expand Up @@ -31,7 +32,9 @@ export const getFieldProps = <T extends Field>(props: Props<T>) => ({
props.data.fn,
]),
name: `schemaData[${props.activeIndex}]['${props.data.fn}']`,
value: props.formik.values.schemaData[props.activeIndex][props.data.fn],
value: props.formik.values.schemaData[props.activeIndex][
props.data.fn
] as ResponseValue<T>,
});

/**
Expand Down
48 changes: 43 additions & 5 deletions editor.planx.uk/src/@planx/components/shared/Schema/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,21 @@ export interface Schema {
max?: number;
}

/**
* Value returned per field, based on field type
*/
export type ResponseValue<T extends Field> = T extends MapField
? Feature[]
: T extends ChecklistField
? string[]
: T extends NumberField
? number
: string;

export type SchemaUserResponse = Record<
Field["data"]["fn"],
string | string[] | any[]
>; // string | string[] | Feature[]
ResponseValue<Field>
>;

/**
* Output data from a form using the useSchema hook
Expand All @@ -130,6 +141,27 @@ export type SchemaUserData = {
schemaData: SchemaUserResponse[];
};

// Type-guards to narrow the type of response values
// Required as we often need to match a value with it's corresponding schema field
export const isNumberFieldResponse = (
response: unknown,
): response is ResponseValue<NumberField> => typeof response === "number";

export const isTextResponse = (
response: unknown,
): response is ResponseValue<TextField | DateField | QuestionField> =>
typeof response === "string";

export const isMapFieldResponse = (
response: unknown,
): response is ResponseValue<MapField> =>
Array.isArray(response) && response[0]?.type === "Feature";

export const isChecklistFieldResponse = (
response: unknown,
): response is ResponseValue<ChecklistField> =>
Array.isArray(response) && !isMapFieldResponse(response);

/**
* For each field in schema, return a map of Yup validation schema
* Matches both the field type and data
Expand Down Expand Up @@ -187,9 +219,15 @@ export const generateValidationSchema = (schema: Schema) => {
export const generateInitialValues = (schema: Schema): SchemaUserResponse => {
const initialValues: SchemaUserResponse = {};
schema.fields.forEach((field) => {
["checklist", "map"].includes(field.type)
? (initialValues[field.data.fn] = [])
: (initialValues[field.data.fn] = "");
switch (field.type) {
case "checklist":
case "map":
initialValues[field.data.fn] = [];
break;
default:
initialValues[field.data.fn] = "";
break;
}
});
return initialValues;
};
3 changes: 2 additions & 1 deletion infrastructure/application/Pulumi.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ config:
secure: AAABAExsXFL7HabeK0Z1oSUJzI2NqVqEmKJ1ojYXyX4Hi8Sbt1Ht9QJc/Yn3cPBAB2r32HKa4HtqqLmfGjS+04lFB/I=
application:hasura-proxy-cpu: "512"
application:hasura-proxy-memory: "1024"
application:hasura-scaling-maximum: "4"
application:hasura-service-scaling-minimum: "1"
application:hasura-service-scaling-maximum: "4"
application:idox-nexus-client:
secure: AAABACdm6IyRjfVPrHLCS5eKQD0ixA2lFC5h04HULwcCXx3j
application:idox-nexus-submission-url: todo
Expand Down
3 changes: 2 additions & 1 deletion infrastructure/application/Pulumi.staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ config:
secure: AAABANHLs3ItPxkteh0chwMP2bKuHO3ovuRLi4FsIrCqerzXVIaTLFDqNR+4KBTeMPz4cnF5tCTwsrJv9GruZdXU+lg=
application:hasura-proxy-cpu: "512"
application:hasura-proxy-memory: "1024"
application:hasura-scaling-maximum: "2"
application:hasura-service-scaling-minimum: "1"
application:hasura-service-scaling-maximum: "2"
application:idox-nexus-client:
secure: AAABABprDQomVM9wJQkTMTVtUKvj9lVVVJLdpEBR5p3ibZYvSMedTOb2jztPa0vm6UCH2hilyOV2fsd+akYd3sP8Up5G26mkEKSLSSN4Nc9fu/Hi3Apn1rXHnw==
application:idox-nexus-submission-url: https://dev.identity.idoxgroup.com/agw/submission-api
Expand Down
27 changes: 14 additions & 13 deletions infrastructure/application/services/hasura.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ export const createHasuraService = async ({
],
healthCheck: {
// hasuraProxy health depends on hasura health
command: [
"CMD-SHELL",
`wget --spider --quiet http://localhost:${HASURA_PROXY_PORT}/healthz || exit 1`,
],
interval: 15,
timeout: 3,
// use wget since busybox applet is included in Alpine base image (curl is not)
command: ["CMD-SHELL", `wget --spider --quiet http://localhost:${HASURA_PROXY_PORT}/healthz || exit 1`],
// generous config; if hasura is saturated/blocking, we give service a chance to scale out before whole task is replaced
interval: 30,
timeout: 15,
retries: 3,
},
environment: [
Expand Down Expand Up @@ -169,8 +168,10 @@ export const createHasuraService = async ({
// TODO: bump awsx to 1.x to use the FargateService scaleConfig option to replace more verbose config below
const hasuraScalingTarget = new aws.appautoscaling.Target("hasura-scaling-target", {
// maxCapacity should consider compute power of the RDS instance which Hasura relies on
maxCapacity: parseInt(config.require("hasura-scaling-maximum")),
minCapacity: 1,
maxCapacity: parseInt(config.require("hasura-service-scaling-maximum")),
// minCapacity should reflect the baseline load expected
// see: https://hasura.io/docs/2.0/deployment/performance-tuning/#scalability
minCapacity: parseInt(config.require("hasura-service-scaling-minimum")),
resourceId: pulumi.interpolate`service/${cluster.cluster.name}/${hasuraService.service.name}`,
scalableDimension: "ecs:service:DesiredCount",
serviceNamespace: "ecs",
Expand All @@ -185,10 +186,10 @@ export const createHasuraService = async ({
predefinedMetricSpecification: {
predefinedMetricType: "ECSServiceAverageCPUUtilization",
},
// scale out early and quickly for responsiveness, but scale in more slowly to avoid thrashing
targetValue: 50.0,
// scale out quickly for responsiveness, but scale in more slowly to avoid thrashing
targetValue: 30.0,
scaleInCooldown: 300,
scaleOutCooldown: 30,
scaleOutCooldown: 60,
},
});

Expand All @@ -201,9 +202,9 @@ export const createHasuraService = async ({
predefinedMetricSpecification: {
predefinedMetricType: "ECSServiceAverageMemoryUtilization",
},
targetValue: 50.0,
targetValue: 30.0,
scaleInCooldown: 300,
scaleOutCooldown: 30,
scaleOutCooldown: 60,
},
});

Expand Down
Loading