Skip to content

Commit

Permalink
implement feature flag (#2043)
Browse files Browse the repository at this point in the history
* implement feature flag

* make sure features is property initialized

* Fix null values

* Remove console log
  • Loading branch information
junjun107 authored Mar 23, 2024
1 parent 20411c3 commit 0878211
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 50 deletions.
46 changes: 18 additions & 28 deletions client/src/components/Admin/Features.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ function createData(featureName, users, featureId) {
})),
};
}

const featureFormValidationSchema = Yup.object({
name: Yup.string().trim().required("Name is required"),
});
const userFormValidationSchema = Yup.object({
email: Yup.string()
.email("Invalid email address")
.required("User email is required"),
});
const Features = () => {
const [selectedRowId, setSelectedRowId] = useState(null);
const [rows, setRows] = useState([]);
Expand All @@ -72,21 +63,17 @@ const Features = () => {

useEffect(() => {
if (featureToLoginData) {
const groupedByFeatureName = featureToLoginData.reduce((acc, curr) => {
acc[curr.feature_name] = [...(acc[curr.feature_name] || []), curr];
return acc;
}, {});
const newRows = Object.entries(groupedByFeatureName).map(
([featureName, users]) => {
const featureId = users[0]?.feature_id;
return createData(featureName, users, featureId);
}
const newRows = featureToLoginData.map((feature) =>
createData(
feature.feature_name,
feature.users || [],
feature.feature_id
)
);
setRows(newRows);
}
}, [featureToLoginData]);

const handleFeatureModalOpen = () => setFeatureModalOpen(true);
const handleModalClose = () => {
setFeatureModalOpen(false);
featureFormik.resetForm();
Expand All @@ -111,7 +98,9 @@ const Features = () => {
initialValues: {
name: "",
},
validationSchema: featureFormValidationSchema,
validationSchema: Yup.object({
name: Yup.string().trim().required("Name is required"),
}),
onSubmit: async (values, { resetForm, setSubmitting }) => {
await featureService.post(values);
featureToLoginRefetch();
Expand All @@ -124,7 +113,11 @@ const Features = () => {
initialValues: {
email: "",
},
validationSchema: userFormValidationSchema,
validationSchema: Yup.object({
email: Yup.string()
.email("Invalid email address")
.required("User email is required"),
}),
onSubmit: async (values, { resetForm, setSubmitting, setFieldError }) => {
try {
const accountResponse = await accountService.getByEmail(values.email);
Expand Down Expand Up @@ -156,10 +149,6 @@ const Features = () => {
},
});

const handleChangePage = (newPage) => {
setPage(newPage);
};

const handleChangeRowsPerPage = (event) => {
setRowsPerPage(+event.target.value);
setPage(0);
Expand All @@ -180,7 +169,7 @@ const Features = () => {
variant="contained"
type="button"
icon="search"
onClick={handleFeatureModalOpen}
onClick={() => setFeatureModalOpen(true)}
>
Add New Feature
</Button>
Expand Down Expand Up @@ -379,7 +368,6 @@ const Features = () => {
</Typography>
<form onSubmit={userFormik.handleSubmit}>
<Box>
{/* <Label id="email" label="Email" /> */}
<TextField
placeholder="Email"
id="email"
Expand Down Expand Up @@ -416,7 +404,9 @@ const Features = () => {
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onPageChange={(newPage) => {
setPage(newPage);
}}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ const ResultsMap = (
}),
[isMobile, isListPanelOpen]
);

return (
<div style={{ position: "relative", height: "100%" }}>
<Map
Expand Down
9 changes: 7 additions & 2 deletions client/src/hooks/useFeatureFlag.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export default function useFeatureFlag(flagName) {
const featureFlags = JSON.parse(process.env.REACT_APP_FEATURE_FLAGS || "[]");
import { useUserContext } from "../contexts/userContext";

export default function useFeatureFlag(flagName) {
const { user } = useUserContext();
if (!user || !user.features) {
return false;
}
const featureFlags = user.features;
return featureFlags.includes(flagName);
}
4 changes: 0 additions & 4 deletions server/app/controllers/account-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import {
import { ClientResponse } from "@sendgrid/mail";

const getAll: RequestHandler<
// route params
never,
// response
Account[],
// req body
never,
// query params
{ tenantId: string }
> = async (req, res) => {
try {
Expand Down
33 changes: 23 additions & 10 deletions server/app/services/account-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,29 @@ const selectByEmail = async (
email: string,
tenantId: string
): Promise<Account> => {
const sql = `select login.id, login.first_name, login.last_name, login.email, login.email_confirmed,
login.password_hash, login.date_created, login.is_global_admin, login.is_global_reporting,
lt.tenant_id, lt.is_admin, lt.is_security_admin, lt.is_data_entry, lt.is_coordinator
from login left outer join
(
select * from login_tenant where tenant_id = $<tenantId>
) as lt on login.id = lt.login_id
where email ilike $<email> `;
const row: Account = await db.one(sql, { email, tenantId: Number(tenantId) });
return camelcaseKeys(row);
const sql = `
select login.id, login.first_name, login.last_name, login.email, login.email_confirmed,login.password_hash,login.date_created, login.is_global_admin, login.is_global_reporting, lt.tenant_id, lt.is_admin, lt.is_security_admin, lt.is_data_entry, lt.is_coordinator,
COALESCE(string_agg(ff.name, ', '), '') as features
From login
LEFT OUTER JOIN (
SELECT * FROM login_tenant WHERE tenant_id = $<tenantId>
) as lt ON login.id = lt.login_id
LEFT JOIN feature_to_login ftl ON login.id = ftl.login_id
LEFT JOIN feature_flag ff ON ftl.feature_id = ff.id
WHERE email ilike $<email>
GROUP BY login.id, login.first_name, login.last_name, login.email, login.email_confirmed, login.date_created, login.is_global_admin, login.is_global_reporting, lt.tenant_id, lt.is_admin, lt.is_security_admin, lt.is_data_entry, lt.is_coordinator`;

const row: Omit<Account, "features"> & { features: string } = await db.one(
sql,
{
email,
tenantId: Number(tenantId),
}
);
return camelcaseKeys({
...row,
features: row.features.split(", ").filter(Boolean) || [],
});
};

const register = async (body: RegisterFields): Promise<AccountResponse> => {
Expand Down
33 changes: 28 additions & 5 deletions server/app/services/feature-to-login-service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import { FeatureToLogin } from "../../types/feature-to-login-types";
import db from "./db";

const getLoginsByFeature = async (): Promise<FeatureToLogin[]> => {
const getLoginsByFeature = async () => {
const sql = `
SELECT u.id as login_id, u.first_name, u.last_name, u.email, ff.name as feature_name, ff.id as feature_id, ftl.id as ftl_id
FROM feature_flag ff
FROM feature_flag ff
LEFT JOIN feature_to_login ftl ON ff.id = ftl.feature_id
LEFT JOIN login u ON u.id = ftl.login_id
ORDER BY ff.id DESC;
`;
const result = await db.manyOrNone(sql);
return result;
`;
const results = await db.manyOrNone(sql);
const groupedByFeature = results.reduce((acc, item) => {
if (!acc.has(item.feature_id)) {
acc.set(item.feature_id, {
feature_id: item.feature_id,
feature_name: item.feature_name,
users: [],
});
}

const feature = acc.get(item.feature_id);
if (item.login_id) {
feature.users.push({
login_id: item.login_id,
first_name: item.first_name,
last_name: item.last_name,
email: item.email,
ftl_id: item.ftl_id,
});
}

return acc;
}, new Map());
const orderedFeatures = Array.from(groupedByFeature.values());
return orderedFeatures;
};

const insert = async (
Expand Down
1 change: 1 addition & 0 deletions server/types/account-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Account extends User {
isSecurityAdmin: boolean;
isDataEntry: boolean;
isCoordinator: boolean;
features: string[];
}

export interface RegisterFields extends User {
Expand Down

0 comments on commit 0878211

Please sign in to comment.