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

tech-story: [M3-8940] - Dev Tools fixes and improvements #11328

Merged
merged 12 commits into from
Dec 6, 2024
55 changes: 29 additions & 26 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,35 @@ export interface Account {

export type BillingSource = 'linode' | 'akamai';

export type AccountCapability =
| 'Akamai Cloud Load Balancer'
| 'Akamai Cloud Pulse'
| 'Block Storage'
| 'Block Storage Encryption'
| 'Cloud Firewall'
| 'CloudPulse'
| 'Disk Encryption'
| 'Kubernetes'
| 'Kubernetes Enterprise'
| 'Linodes'
| 'LKE HA Control Planes'
| 'LKE Network Access Control List (IP ACL)'
| 'Machine Images'
| 'Managed Databases'
| 'Managed Databases Beta'
| 'NETINT Quadra T1U'
| 'NodeBalancers'
| 'Object Storage Access Key Regions'
| 'Object Storage Endpoint Types'
| 'Object Storage'
| 'Placement Group'
| 'SMTP Enabled'
| 'Support Ticket Severity'
| 'Vlans'
| 'VPCs';
export const accountCapabilities = [
'Akamai Cloud Load Balancer',
'Akamai Cloud Pulse',
'Block Storage',
'Block Storage Encryption',
'Cloud Firewall',
'CloudPulse',
'Disk Encryption',
'Kubernetes',
'Kubernetes Enterprise',
'Linodes',
'LKE HA Control Planes',
'LKE Network Access Control List (IP ACL)',
'Machine Images',
'Managed Databases',
'Managed Databases Beta',
'NETINT Quadra T1U',
'NodeBalancers',
'Object Storage Access Key Regions',
'Object Storage Endpoint Types',
'Object Storage',
'Placement Group',
'SMTP Enabled',
'Support Ticket Severity',
'Vlans',
'VPCs',
] as const;

export type AccountCapability = typeof accountCapabilities[number];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes nothing to the types, just allows to export the values


export interface AccountAvailability {
region: string; // will be slug of dc (matches id field of region object returned by API)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Dev Tools fixes and improvements ([#11328](https://github.com/linode/manager/pull/11328))
136 changes: 111 additions & 25 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,85 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'iam', label: 'Identity and Access Beta' },
];

interface RenderFlagItemProps {
label: string;
onCheck: (e: React.ChangeEvent, flag: string) => void;
path: string;
searchTerm: string;
value: boolean | object | string | undefined;
}

const renderFlagItem = ({
label,
onCheck,
path = '',
searchTerm,
value,
}: RenderFlagItemProps) => {
const isObject = typeof value === 'object' && value !== null;

if (!isObject) {
return (
<label title={label}>
<input
checked={Boolean(value)}
onChange={(e) => onCheck(e, path || label)}
type="checkbox"
/>
{label}
</label>
);
}

const sortedEntries = Object.entries(value).sort((a, b) =>
a[0].localeCompare(b[0])
);

return (
<ul>
<details>
<summary>{label}</summary>
{sortedEntries.map(([key, nestedValue], index) => (
<li key={`${key}-${index}`}>
{renderFlagItem({
label: key,
onCheck,
path: path ? `${path}.${key}` : key,
searchTerm,
value: nestedValue,
})}
</li>
))}
</details>
</ul>
);
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing recursiveness so the JSON flag can be as deep as needed and represented here

Left to do: handle the cases for when a flag is something else than a boolean, which we rarely do


const renderFlagItems = (
flags: Partial<Flags>,
onCheck: (e: React.ChangeEvent, flag: string) => void
onCheck: (e: React.ChangeEvent, flag: string) => void,
searchTerm: string
) => {
return options.map((option) => {
const sortedOptions = options.sort((a, b) => a.label.localeCompare(b.label));
return sortedOptions.map((option) => {
const flagValue = flags[option.flag];
const isChecked =
typeof flagValue === 'object' && 'enabled' in flagValue
? Boolean(flagValue.enabled)
: Boolean(flagValue);
const isSearchMatch = option.label
.toLowerCase()
.includes(searchTerm.toLowerCase());

if (!isSearchMatch) {
return null;
}

return (
<li key={option.flag}>
<input
style={{
marginRight: 12,
}}
checked={isChecked}
onChange={(e) => onCheck(e, option.flag)}
type="checkbox"
/>
<span title={option.label}>{option.label}</span>
{renderFlagItem({
label: option.label,
onCheck,
path: option.flag,
searchTerm,
value: flagValue,
})}
</li>
);
});
Expand All @@ -70,6 +127,7 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => {
const dispatch: Dispatch = useDispatch();
const flags = useFlags();
const ldFlags = ldUseFlags();
const [searchTerm, setSearchTerm] = React.useState('');

React.useEffect(() => {
const storedFlags = getStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY);
Expand All @@ -82,15 +140,33 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => {
e: React.ChangeEvent<HTMLInputElement>,
flag: keyof FlagSet
) => {
const currentFlag = flags[flag];
const updatedValue =
typeof currentFlag == 'object' && 'enabled' in currentFlag
? { ...currentFlag, enabled: e.target.checked } // If current flag is an object, update 'enabled' key
: e.target.checked;
const updatedFlags = {
...getStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY),
[flag]: updatedValue,
};
const updatedValue = e.target.checked;
const storedFlags = getStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY) || {};

const flagParts = flag.split('.');
const updatedFlags = { ...storedFlags };

// If the flag is not a nested flag, update it directly
if (flagParts.length === 1) {
updatedFlags[flag] = updatedValue;
} else {
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
// If the flag is a nested flag, update the specific property that changed
const [parentKey, childKey] = flagParts;
const currentParentValue = ldFlags[parentKey];
const existingValues = storedFlags[parentKey] || {};

// Only update the specific property that changed
updatedFlags[parentKey] = {
...currentParentValue, // Keep original LD values
...existingValues, // Apply any existing stored overrides
[childKey]: updatedValue, // Apply the new change
};
}

updateFlagStorage(updatedFlags);
};

const updateFlagStorage = (updatedFlags: object) => {
dispatch(setMockFeatureFlags(updatedFlags));
setStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY, JSON.stringify(updatedFlags));
abailly-akamai marked this conversation as resolved.
Show resolved Hide resolved
};
Expand All @@ -103,6 +179,10 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => {
setStorage(MOCK_FEATURE_FLAGS_STORAGE_KEY, '');
};

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};

return (
<div className="dev-tools__tool">
<div className="dev-tools__tool__header">
Expand All @@ -112,7 +192,13 @@ export const FeatureFlagTool = withFeatureFlagProvider(() => {
</div>
<div className="dev-tools__tool__body">
<div className="dev-tools__list-box">
<ul>{renderFlagItems(flags, handleCheck)}</ul>
<input
onChange={handleSearch}
placeholder="Search feature flags"
style={{ margin: 12 }}
type="text"
/>
<ul>{renderFlagItems(flags, handleCheck, searchTerm)}</ul>
</div>
</div>
<div className="dev-tools__tool__footer">
Expand Down
Loading
Loading