Skip to content

Commit

Permalink
Merge pull request #6 from Flagsmith/chore/update-endpoints-to-limit-…
Browse files Browse the repository at this point in the history
…features

fix: Update endpoints to limit features
  • Loading branch information
novakzaballa authored Apr 24, 2024
2 parents fa99526 + 2bf81d1 commit f7cbdac
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 94 deletions.
172 changes: 95 additions & 77 deletions flagsmith-jira-app/src/IssueFlagPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import {
EnvironmentModel,
FLAGSMITH_APP,
FeatureModel,
FeatureStateValue,
EnvironmentFeatureState,
FlagModel,
fetchEnvironments,
fetchFeatures,
fetchFlags,
fetchFeatureState,
} from "./flagsmith";
import { canEditIssue, readFeatureIds, readProjectId, writeFeatureIds } from "./jira";
import { readApiKey, readOrganisationId } from "./storage";
Expand All @@ -40,13 +40,15 @@ import { readApiKey, readOrganisationId } from "./storage";
ForgeUI;

type IssueFlagFormProps = {
features: FeatureModel[];
featureIds: string[];
flagsmithFeatures: FeatureModel[];
jiraFeatureIds: string[];
onAdd: (featureId: string) => Promise<void>;
};

const IssueFlagForm = ({ features, featureIds, onAdd }: IssueFlagFormProps) => {
const addableFeatures = features.filter((feature) => !featureIds.includes(String(feature.id)));
const IssueFlagForm = ({ flagsmithFeatures, jiraFeatureIds, onAdd }: IssueFlagFormProps) => {
const addableFeatures = flagsmithFeatures.filter(
(feature) => !jiraFeatureIds.includes(String(feature.id)),
);

return (
<Fragment>
Expand All @@ -71,45 +73,86 @@ const IssueFlagForm = ({ features, featureIds, onAdd }: IssueFlagFormProps) => {

type IssueFlagTableProps = {
projectUrl: string;
apiKey: string;
environments: EnvironmentModel[];
features: FeatureModel[];
flags: Record<string, FlagModel[]>;
featureIds: string[];
flagsmithFeatures: FeatureModel[];
jiraFeatureIds: string[];
onRemove: (featureId: string) => Promise<void>;
canEdit: boolean;
};

const IssueFlagTable = ({
projectUrl,
apiKey,
environments,
features,
flags,
featureIds,
flagsmithFeatures,
jiraFeatureIds,
onRemove,
canEdit,
}: IssueFlagTableProps) => {
if (featureIds.length === 0) {
const [environmentFlags, setEnvironmentFlags] = useState<any>([]);

useEffect(async () => {
// Filtered features by comparing their IDs with the feature IDs stored in Jira.
const flagsmithfeaturesFiltered = flagsmithFeatures.filter((f) =>
jiraFeatureIds.includes(String(f.id)),
);
try {
if (environments.length > 0 && flagsmithfeaturesFiltered.length > 0) {
const featureState: any = {};
// Iterate over each filtered feature.
for (const feature of flagsmithfeaturesFiltered) {
// Initialize an object to store the state of the feature.
featureState[String(feature.name)] = {
name: feature.name,
feature_id: feature.id,
description: feature.description,
environments: [],
};
for (const environment of environments) {
// Obtain the feature states of the filtered features.
const ffData = await fetchFeatureState({
apiKey,
featureName: feature.name,
envAPIKey: String(environment.api_key),
});
ffData.name = environment.name;
ffData.api_key = String(environment.api_key);
// Add the feature state data to the feature state object.
featureState[String(feature.name)].environments.push(ffData);
}
}
const ffArray = Object.keys(featureState).map((featureName) => featureState[featureName]);
setEnvironmentFlags(ffArray);
} else {
setEnvironmentFlags([]);
}
} catch (error) {
if (!(error instanceof Error)) throw error;
}
}, [apiKey, jiraFeatureIds, environments, flagsmithFeatures]);

if (jiraFeatureIds.length === 0) {
return <Text>No feature flags are linked to this issue.</Text>;
}

let first = true;
return (
<Fragment>
{featureIds.map((featureId) => {
const feature = features.find((each) => String(each.id) === String(featureId));
{environmentFlags.map((environmentFlag: FlagModel) => {
// add vertical space to separate the previous feature's Remove button from this feature
const spacer = first ? null : <Text>&nbsp;</Text>;
first = false;
return (
!!feature && (
<Fragment key={featureId}>
!!environmentFlag && (
<Fragment key={environmentFlag.feature_id}>
{canEdit && spacer}
<Text>
<Strong>
{feature.name}
{feature.description ? ": " : ""}
{environmentFlag.name}
{environmentFlag.description ? ": " : ""}
</Strong>
{feature.description}
{environmentFlag.description}
</Text>
<Table>
<Head>
Expand All @@ -126,36 +169,30 @@ const IssueFlagTable = ({
<Text>Last updated</Text>
</Cell>
</Head>
{environments.map((environment) => {
const environmentFlags = flags[String(environment.id)] ?? [];
// get the default state for this environment
const flag = environmentFlags.find(
(each) =>
String(each.feature) === String(featureId) &&
each.feature_segment === null &&
each.identity === null,
);
{environmentFlag?.environments.map((flag: EnvironmentFeatureState) => {
if (!flag) return null;
const value: Partial<FeatureStateValue> = flag.feature_state_value ?? {};
// count variations/overrides
const variations = flag.multivariate_feature_state_values.length;
const segments = environmentFlags.filter(
(each) =>
String(each.feature) === String(featureId) && each.feature_segment !== null,
(each: EnvironmentFeatureState) =>
String(each.feature) === String(environmentFlag.feature_id) &&
each.feature_segment !== null,
).length;
const identities = environmentFlags.filter(
(each) => String(each.feature) === String(featureId) && each.identity !== null,
(each: EnvironmentFeatureState) =>
String(each.feature) === String(environmentFlag.feature_id) &&
each.identity !== null,
).length;
return (
<Row key={String(featureId)}>
<Row key={String(`${environmentFlag.feature_id}`)}>
<Cell>
<Text>
<Link
href={`${projectUrl}/environment/${environment.api_key}/features?feature=${featureId}`}
href={`${projectUrl}/environment/${flag.api_key}/features?feature=${environmentFlag.feature_id}`}
appearance="link"
openNewTab
>
{environment.name}
{flag.name}
</Link>
</Text>
{variations > 0 && (
Expand Down Expand Up @@ -192,15 +229,7 @@ const IssueFlagTable = ({
</Text>
</Cell>
<Cell>
{value.type === "unicode" ? (
<Text>{JSON.stringify(value.string_value)}</Text>
) : value.type === "int" ? (
<Text>{JSON.stringify(value.integer_value)}</Text>
) : value.type === "bool" ? (
<Text>{JSON.stringify(!!value.boolean_value)}</Text>
) : (
<Text>Unknown type: {value.type}</Text>
)}
<Text>{flag.feature_state_value}</Text>
</Cell>
<Cell>
<Text>
Expand All @@ -213,7 +242,10 @@ const IssueFlagTable = ({
</Table>
{canEdit && (
<ButtonSet>
<Button text="Unlink from issue" onClick={() => onRemove(featureId)} />
<Button
text="Unlink from issue"
onClick={() => onRemove(`${environmentFlag.feature_id}`)}
/>
</ButtonSet>
)}
</Fragment>
Expand All @@ -224,15 +256,13 @@ const IssueFlagTable = ({
);
};

type Flags = Record<string, FlagModel[]>;

type IssueFlagPanelProps = {
setError: (error: Error) => void;
jiraContext: JiraContext;
apiKey: string;
organisationId: string;
projectId: string | undefined;
featureIds: string[] | undefined;
jiraFeatureIds: string[] | undefined;
canEdit: boolean;
};

Expand All @@ -245,11 +275,9 @@ const IssueFlagPanel = ({
...props
}: IssueFlagPanelProps) => {
// set initial state
const [featureIds, setFeatureIds] = useState(props.featureIds ?? []);
const [jiraFeatureIds, setJiraFeatureIds] = useState(props.jiraFeatureIds ?? []);
const [environments, setEnvironments] = useState([] as EnvironmentModel[]);
const [features, setFeatures] = useState([] as FeatureModel[]);
const [flags, setFlags] = useState({} as Flags);

// load environments and features
useEffect(async () => {
try {
Expand All @@ -263,47 +291,37 @@ const IssueFlagPanel = ({
);
// update form state
setFeatures(features);
if (environments.length > 0 && features.length > 0) {
// obtain flags from API
const flags: Flags = {};
for (const environment of environments) {
flags[String(environment.id)] = await fetchFlags({
apiKey,
environmentId: String(environment.id),
});
}
// update form state
setFlags(flags);
}
} catch (error) {
if (!(error instanceof Error)) throw error;
setError(error);
}
}, [apiKey, String(projectId)]);

const onChange = async (featureIds: string[]) => {
const onChange = async (jiraFeatureIds: string[]) => {
// persist to storage
await writeFeatureIds(jiraContext, featureIds);
await writeFeatureIds(jiraContext, jiraFeatureIds);
// update state
setFeatureIds(featureIds);
setJiraFeatureIds(jiraFeatureIds);
};
const onAdd = (featureId: string) => onChange([...featureIds, featureId]);
const onAdd = (featureId: string) => onChange([...jiraFeatureIds, featureId]);
const onRemove = (featureId: string) =>
onChange(featureIds.filter((each) => String(each) !== featureId));
onChange(jiraFeatureIds.filter((each) => String(each) !== featureId));

const projectUrl = `${FLAGSMITH_APP}/project/${projectId}`;
return (
<Fragment>
<IssueFlagTable
projectUrl={projectUrl}
apiKey={apiKey}
environments={environments}
features={features}
flags={flags}
featureIds={featureIds}
flagsmithFeatures={features}
jiraFeatureIds={jiraFeatureIds}
onRemove={onRemove}
canEdit={canEdit}
/>
{canEdit && <IssueFlagForm features={features} featureIds={featureIds} onAdd={onAdd} />}
{canEdit && (
<IssueFlagForm flagsmithFeatures={features} jiraFeatureIds={jiraFeatureIds} onAdd={onAdd} />
)}
</Fragment>
);
};
Expand All @@ -329,9 +347,9 @@ export default () => {
const [organisationId, setOrganisationId] = useState(readOrganisationId);
const jiraContext = useJiraContext();
const [projectId, setProjectId] = useState(() => readProjectId(jiraContext));
const [featureIds, setFeatureIds] = useState(() => readFeatureIds(jiraContext));
const [jiraFeatureIds, setJiraFeatureIds] = useState(() => readFeatureIds(jiraContext));
const [canEdit, setCanEdit] = useState(() => canEditIssue(jiraContext));
const [editing, setEditing] = useState(!(featureIds ?? []).length);
const [editing, setEditing] = useState(!(jiraFeatureIds ?? []).length);

const actions = canEdit
? [<EditAction key="edit" editing={editing} setEditing={setEditing} />]
Expand All @@ -346,15 +364,15 @@ export default () => {
jiraContext={jiraContext}
organisationId={organisationId}
projectId={projectId}
featureIds={featureIds}
jiraFeatureIds={jiraFeatureIds}
canEdit={canEdit && editing}
/>
)}
onRetry={async () => {
setApiKey(await readApiKey());
setOrganisationId(await readOrganisationId());
setProjectId(await readProjectId(jiraContext));
setFeatureIds(await readFeatureIds(jiraContext));
setJiraFeatureIds(await readFeatureIds(jiraContext));
setCanEdit(await canEditIssue(jiraContext));
}}
/>
Expand Down
Loading

0 comments on commit f7cbdac

Please sign in to comment.