Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,195 @@
|
||||
import { FeatureFlag, FeatureFlagFilter, Role, Tenant } from "@toolctrl/db/prisma-browser";
|
||||
import { useNavigation, useNavigate, Form } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
|
||||
import LoadingButton from "~/components/ui/buttons/LoadingButton";
|
||||
import InputText from "~/components/ui/input/InputText";
|
||||
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
|
||||
import SessionFilterModal from "~/modules/shared/components/SessionFilterModal";
|
||||
import { FeatureFlagsFilterType, FeatureFlagsFilterTypes } from "../dtos/FeatureFlagsFilterTypes";
|
||||
import { SubscriptionProductDto } from "~/application/dtos/subscriptions/SubscriptionProductDto";
|
||||
import { UserSimple } from "~/utils/db/users.db.server";
|
||||
import { adminPath } from "~/utils/app/CanonicalUrlUtils";
|
||||
import { useLocaleFormatters } from "~/locale/format";
|
||||
|
||||
export default function FeatureFlagForm({
|
||||
item,
|
||||
metadata,
|
||||
onDelete,
|
||||
}: {
|
||||
item?: FeatureFlag & { filters: FeatureFlagFilter[] };
|
||||
metadata: {
|
||||
users: UserSimple[];
|
||||
tenants: Tenant[];
|
||||
subscriptionProducts: SubscriptionProductDto[];
|
||||
roles: Role[];
|
||||
analytics: {
|
||||
via: { name: string; count: number }[];
|
||||
httpReferrer: { name: string; count: number }[];
|
||||
browser: { name: string; count: number }[];
|
||||
os: { name: string; count: number }[];
|
||||
source: { name: string; count: number }[];
|
||||
medium: { name: string; count: number }[];
|
||||
campaign: { name: string; count: number }[];
|
||||
};
|
||||
};
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const formatters = useLocaleFormatters();
|
||||
const navigation = useNavigation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [showFilterModal, setShowFilterModal] = useState<{
|
||||
item?: { type: FeatureFlagsFilterType; value: string | null; action: string | null };
|
||||
idx?: number;
|
||||
}>();
|
||||
|
||||
const [name, setName] = useState(item?.name || "");
|
||||
const [description, setDescription] = useState(item?.description || "");
|
||||
const [filters, setFilters] = useState<{ type: FeatureFlagsFilterType; value: string | null; action: string | null }[]>(
|
||||
item?.filters.map((f) => {
|
||||
return {
|
||||
type: f.type as FeatureFlagsFilterType,
|
||||
value: f.value,
|
||||
action: f.action,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) {
|
||||
// setName(StringUtils.toCamelCase(description.toLowerCase()));
|
||||
if (name === "currency") {
|
||||
setDescription(t("featureFlags.form.defaults.currencyDescription"));
|
||||
}
|
||||
}
|
||||
}, [item, name, t]);
|
||||
|
||||
function onSaveFilter(item: { type: FeatureFlagsFilterType; value: string | null; action: string | null }) {
|
||||
const idx = showFilterModal?.idx;
|
||||
if (idx !== undefined) {
|
||||
filters[idx] = item;
|
||||
} else {
|
||||
filters.push(item);
|
||||
}
|
||||
setFilters([...filters]);
|
||||
setShowFilterModal(undefined);
|
||||
}
|
||||
function getActionName() {
|
||||
return item ? "edit" : "new";
|
||||
}
|
||||
|
||||
// const hasDefaultFlag = ["currency"].includes(name);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SlideOverWideEmpty
|
||||
title={item ? t("featureFlags.form.editTitle") : t("featureFlags.form.createTitle")}
|
||||
description={item ? t("featureFlags.form.editDescription") : t("featureFlags.form.createDescription")}
|
||||
open={true}
|
||||
onClose={() => {
|
||||
navigate(adminPath("feature-flags/flags", { mode: "host-aware" }));
|
||||
}}
|
||||
className="sm:max-w-sm"
|
||||
overflowYScroll={true}
|
||||
>
|
||||
<Form method="post" className="inline-block w-full overflow-hidden p-1 text-left align-bottom sm:align-middle">
|
||||
<input type="hidden" name="action" value={getActionName()} />
|
||||
{filters.map((filter, index) => {
|
||||
return <input type="hidden" name="filters[]" value={JSON.stringify(filter)} key={index} />;
|
||||
})}
|
||||
|
||||
<div className="space-y-2">
|
||||
<InputText
|
||||
name="name"
|
||||
title={t("shared.name")}
|
||||
value={name}
|
||||
setValue={setName}
|
||||
required
|
||||
hint={<span>{t("featureFlags.form.hints.internalName")}</span>}
|
||||
/>
|
||||
<InputText autoFocus name="description" title={t("shared.title")} value={description} setValue={setDescription} required />
|
||||
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between space-x-2 text-xs">
|
||||
<label className="text-muted-foreground font-medium">{t("featureFlags.filters")}</label>
|
||||
<button type="button" onClick={() => setFilters([])} className="text-muted-foreground hover:text-muted-foreground">
|
||||
{t("shared.clear")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-gray-800">
|
||||
{filters.map((filter, idx) => {
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => setShowFilterModal({ item: filter, idx })}
|
||||
className="border-border hover:border-border relative block w-full rounded-lg border-2 border-dashed p-3 text-center focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-1 truncate">
|
||||
<div className="truncate font-medium">{filter.type}</div>
|
||||
<div className="truncate italic">
|
||||
{filter.value === null ? (
|
||||
<span className="text-red-500">null</span>
|
||||
) : filter.type === "user.createdAfter" || filter.type === "user.createdBefore" ? (
|
||||
formatters.date(filter.value, { dateStyle: "medium" })
|
||||
) : (
|
||||
filter.value
|
||||
)}
|
||||
{filter.action && ` → ${filter.action}`}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFilterModal({ item: undefined, idx: undefined })}
|
||||
className="border-border hover:border-border relative block w-full rounded-lg border-2 border-dashed p-3 text-center focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:outline-none"
|
||||
>
|
||||
<span className="block text-sm font-medium text-gray-900">{t("featureFlags.form.actions.addFilter")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-between space-x-2 sm:mt-6">
|
||||
<div>
|
||||
{onDelete && (
|
||||
<ButtonSecondary disabled={navigation.state === "submitting"} type="button" className="w-full" onClick={onDelete} destructive>
|
||||
<div>{t("shared.delete")}</div>
|
||||
</ButtonSecondary>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<ButtonSecondary onClick={() => navigate(adminPath("feature-flags/flags", { mode: "host-aware" }))}>{t("shared.cancel")}</ButtonSecondary>
|
||||
<LoadingButton actionName={getActionName()} type="submit" disabled={navigation.state === "submitting"}>
|
||||
{t("shared.save")}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<SessionFilterModal
|
||||
flagName={name}
|
||||
filters={FeatureFlagsFilterTypes.map((f) => f.toString())}
|
||||
open={showFilterModal !== undefined}
|
||||
item={showFilterModal?.item}
|
||||
idx={showFilterModal?.idx}
|
||||
onClose={() => setShowFilterModal(undefined)}
|
||||
onSave={({ type, value, action }) => onSaveFilter({ type: type as FeatureFlagsFilterType, value, action: action ?? null })}
|
||||
metadata={metadata}
|
||||
onRemove={(idx) => {
|
||||
filters.splice(idx, 1);
|
||||
setFilters([...filters]);
|
||||
}}
|
||||
/>
|
||||
</SlideOverWideEmpty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useLocation, useMatches } from "react-router";
|
||||
import { Fragment, ReactNode, useEffect } from "react";
|
||||
import { useRootData } from "~/utils/data/useRootData";
|
||||
import AnalyticsHelper from "~/utils/helpers/AnalyticsHelper";
|
||||
|
||||
interface Props {
|
||||
flag: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
export default function FeatureFlagTracker({ flag, children }: Props) {
|
||||
let location = useLocation();
|
||||
const rootData = useRootData();
|
||||
const matches = useMatches();
|
||||
|
||||
useEffect(() => {
|
||||
async function track() {
|
||||
const routeMatch = matches.find((m) => m.pathname == location.pathname);
|
||||
AnalyticsHelper.addEvent({
|
||||
url: location.pathname,
|
||||
route: routeMatch?.id,
|
||||
rootData,
|
||||
action: flag,
|
||||
category: "featureFlag",
|
||||
label: "",
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
|
||||
if (rootData?.featureFlags.includes(flag)) {
|
||||
track();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [flag]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
Reference in new issue
Block a user