Skip to content

Commit

Permalink
tech-story: [M3-8940] - Dev Tools fixes and improvements (#11328)
Browse files Browse the repository at this point in the history
* nested JSON flags

* fix overflow

* Styling updates

* save progress

* save progress

* Fix types

* Wrapping up account customization

* form improvements profile

* cleanup

* Added changeset: Dev Tools fixes and improvements

* fix flake

* feedback @jaalah-akamai
  • Loading branch information
abailly-akamai authored Dec 6, 2024
1 parent 04f241d commit 5ac188d
Show file tree
Hide file tree
Showing 28 changed files with 1,292 additions and 340 deletions.
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];

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))
File renamed without changes.
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>
);
};

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

0 comments on commit 5ac188d

Please sign in to comment.