feat: view config feature flag (#13171)
* feat: add view_configurations feature flag - Add feature flag provider and hooks to admin dashboard - Add backend API endpoint for feature flags - Create view_configurations feature flag (disabled by default) - Update order list table to use legacy version when flag is disabled - Can be enabled with MEDUSA_FF_VIEW_CONFIGURATIONS=true env var * fix: naming * fix: feature flags unauthenticated * fix: add test
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("GET /admin/feature-flags", () => {
|
||||||
|
it("should return feature flags when unauthenticated", async () => {
|
||||||
|
const response = await api.get("/admin/feature-flags")
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data).toHaveProperty("feature_flags")
|
||||||
|
expect(response.data.feature_flags).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
view_configurations: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return feature flags when authenticated as admin", async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
|
||||||
|
const response = await api.get("/admin/feature-flags", adminHeaders)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data).toHaveProperty("feature_flags")
|
||||||
|
expect(response.data.feature_flags).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
view_configurations: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
|
|
||||||
|
export type FeatureFlags = {
|
||||||
|
view_configurations?: boolean
|
||||||
|
[key: string]: boolean | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFeatureFlags = () => {
|
||||||
|
return useQuery<FeatureFlags>({
|
||||||
|
queryKey: ["admin", "feature-flags"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>("/admin/feature-flags", {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.feature_flags
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { createContext, useContext } from "react"
|
||||||
|
import { useFeatureFlags, FeatureFlags } from "../../hooks/api/feature-flags"
|
||||||
|
|
||||||
|
interface FeatureFlagContextValue {
|
||||||
|
flags: FeatureFlags
|
||||||
|
isLoading: boolean
|
||||||
|
isFeatureEnabled: (flag: keyof FeatureFlags) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null)
|
||||||
|
|
||||||
|
export const useFeatureFlag = (flag: keyof FeatureFlags): boolean => {
|
||||||
|
const context = useContext(FeatureFlagContext)
|
||||||
|
if (!context) {
|
||||||
|
// If no context, assume feature is disabled
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return context.isFeatureEnabled(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFeatureFlagContext = () => {
|
||||||
|
const context = useContext(FeatureFlagContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useFeatureFlagContext must be used within FeatureFlagProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeatureFlagProviderProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureFlagProvider: React.FC<FeatureFlagProviderProps> = ({ children }) => {
|
||||||
|
const { data: flags = {}, isLoading, error } = useFeatureFlags()
|
||||||
|
|
||||||
|
const isFeatureEnabled = (flag: keyof FeatureFlags): boolean => {
|
||||||
|
const enabled = flags[flag] === true
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureFlagContext.Provider value={{ flags, isLoading, isFeatureEnabled }}>
|
||||||
|
{children}
|
||||||
|
</FeatureFlagContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { queryClient } from "../lib/query-client"
|
|||||||
import { ExtensionProvider } from "./extension-provider"
|
import { ExtensionProvider } from "./extension-provider"
|
||||||
import { I18nProvider } from "./i18n-provider"
|
import { I18nProvider } from "./i18n-provider"
|
||||||
import { ThemeProvider } from "./theme-provider"
|
import { ThemeProvider } from "./theme-provider"
|
||||||
|
import { FeatureFlagProvider } from "./feature-flag-provider"
|
||||||
|
|
||||||
type ProvidersProps = PropsWithChildren<{
|
type ProvidersProps = PropsWithChildren<{
|
||||||
api: DashboardApp["api"]
|
api: DashboardApp["api"]
|
||||||
@@ -20,9 +21,11 @@ export const Providers = ({ api, children }: ProvidersProps) => {
|
|||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<I18n />
|
<FeatureFlagProvider>
|
||||||
<I18nProvider>{children}</I18nProvider>
|
<I18n />
|
||||||
<Toaster />
|
<I18nProvider>{children}</I18nProvider>
|
||||||
|
<Toaster />
|
||||||
|
</FeatureFlagProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const ConfigurableOrderListTable = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading>{t("orders.domain")}</Heading>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<p className="text-ui-fg-muted">
|
||||||
|
View configurations feature is enabled. Full implementation coming soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+9
@@ -8,6 +8,8 @@ import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-ord
|
|||||||
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
|
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
|
||||||
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||||
|
import { ConfigurableOrderListTable } from "./configurable-order-list-table"
|
||||||
|
|
||||||
import { DEFAULT_FIELDS } from "../../const"
|
import { DEFAULT_FIELDS } from "../../const"
|
||||||
|
|
||||||
@@ -15,6 +17,13 @@ const PAGE_SIZE = 20
|
|||||||
|
|
||||||
export const OrderListTable = () => {
|
export const OrderListTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||||
|
|
||||||
|
// If feature flag is enabled, use the new configurable table
|
||||||
|
if (isViewConfigEnabled) {
|
||||||
|
return <ConfigurableOrderListTable />
|
||||||
|
}
|
||||||
|
|
||||||
const { searchParams, raw } = useOrderTableQuery({
|
const { searchParams, raw } = useOrderTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
export const AUTHENTICATE = false
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse<{ feature_flags: Record<string, boolean> }>
|
||||||
|
) => {
|
||||||
|
const featureFlagRouter = req.scope.resolve(
|
||||||
|
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
|
||||||
|
) as any
|
||||||
|
|
||||||
|
const flags = featureFlagRouter.listFlags()
|
||||||
|
|
||||||
|
// Convert array of flags to a simple key-value object
|
||||||
|
const featureFlags: Record<string, boolean> = {}
|
||||||
|
flags.forEach(flag => {
|
||||||
|
featureFlags[flag.key] = flag.value
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ feature_flags: featureFlags })
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { FlagSettings } from "@medusajs/framework/feature-flags"
|
||||||
|
|
||||||
|
const ViewConfigurationsFeatureFlag: FlagSettings = {
|
||||||
|
key: "view_configurations",
|
||||||
|
default_val: false,
|
||||||
|
env_key: "MEDUSA_FF_VIEW_CONFIGURATIONS",
|
||||||
|
description: "[WIP] Enable view configurations for data tables",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewConfigurationsFeatureFlag
|
||||||
Reference in New Issue
Block a user