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

feat(feature-mgmt): CRUD - U implemented end to end #27254

Draft
wants to merge 32 commits into
base: feature-management-backend-setup
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3957382
stash
havenbarnes Dec 9, 2024
68869d5
stash
havenbarnes Dec 9, 2024
21cd4d6
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 10, 2024
ea3a74d
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 18, 2024
1e7875c
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 18, 2024
12d2260
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 18, 2024
c40d009
Merge branch 'master' of https://github.com/PostHog/posthog into feat…
havenbarnes Dec 18, 2024
0c9c623
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 18, 2024
a67c53c
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 18, 2024
b7aada3
stash
havenbarnes Dec 19, 2024
7839cbd
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 19, 2024
4bd0712
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 19, 2024
aed5ff1
Merge branch 'master' of https://github.com/PostHog/posthog into feat…
havenbarnes Dec 20, 2024
8ede33e
stash
havenbarnes Dec 20, 2024
1c9ceab
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 20, 2024
97aafbd
Merge branch 'master' of https://github.com/PostHog/posthog into feat…
havenbarnes Dec 20, 2024
627b145
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 20, 2024
d6ef36d
stash a change
havenbarnes Dec 30, 2024
817fa37
Merge branch 'master' of https://github.com/PostHog/posthog into feat…
havenbarnes Dec 30, 2024
59a3be4
stash
havenbarnes Dec 30, 2024
3c835ad
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 30, 2024
3518051
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Dec 30, 2024
0a087e6
Set up creation and deletion e2e
havenbarnes Jan 3, 2025
607b8f5
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Jan 3, 2025
57f7247
Clean up, fix form resetting
havenbarnes Jan 3, 2025
7ca740d
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Jan 3, 2025
a424ac7
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Jan 3, 2025
9fc01c9
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Jan 8, 2025
9c3fedd
Merge branch 'feature-management-backend-setup' of https://github.com…
havenbarnes Jan 8, 2025
2cf7a5d
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 8, 2025
212823e
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 8, 2025
d792843
Merge branch 'feature-management-backend-setup' into feature-manageme…
havenbarnes Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity'
import { apiStatusLogic } from 'lib/logic/apiStatusLogic'
import { objectClean, toParams } from 'lib/utils'
import posthog from 'posthog-js'
import { NewFeatureForm } from 'scenes/feature-management/featureManagementEditLogic'
import { RecordingComment } from 'scenes/session-recordings/player/inspector/playerInspectorLogic'
import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic'

Expand Down Expand Up @@ -57,6 +58,7 @@ import {
FeatureFlagAssociatedRoleType,
FeatureFlagStatusResponse,
FeatureFlagType,
FeatureType,
Group,
GroupListParams,
HogFunctionIconResponse,
Expand Down Expand Up @@ -632,6 +634,15 @@ class ApiRequest {
return this.annotations(teamId).addPathComponent(id)
}

// # Feature managment
public features(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('features')
}

public feature(id: FeatureType['id'], teamId?: TeamType['id']): ApiRequest {
return this.features(teamId).addPathComponent(id)
}

// # Feature flags
public featureFlags(teamId?: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('feature_flags')
Expand Down Expand Up @@ -1039,6 +1050,29 @@ const api = {
},
},

features: {
async list(
teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()
): Promise<CountedPaginatedResponse<FeatureType>> {
return await new ApiRequest().features(teamId).get()
},
async get(id: FeatureType['id'], teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()): Promise<FeatureType> {
return await new ApiRequest().feature(id, teamId).get()
},
async create(
feature: NewFeatureForm,
teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()
): Promise<FeatureType> {
return await new ApiRequest().features(teamId).create({ data: feature })
},
async update(
feature: FeatureType,
teamId: TeamType['id'] = ApiConfig.getCurrentTeamId()
): Promise<FeatureType> {
return await new ApiRequest().feature(feature.id, teamId).update({ data: feature })
},
},

featureFlags: {
async get(id: FeatureFlagType['id']): Promise<FeatureFlagType> {
return await new ApiRequest().featureFlag(id).get()
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/scenes/appScenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const appScenes: Record<Scene, () => any> = {
[Scene.ExperimentsSavedMetric]: () => import('./experiments/SavedMetrics/SavedMetric'),
[Scene.Experiment]: () => import('./experiments/Experiment'),
[Scene.FeatureFlags]: () => import('./feature-flags/FeatureFlags'),
[Scene.FeatureManagement]: () => import('./feature-flags/FeatureManagement'),
[Scene.FeatureManagement]: () => import('./feature-management/FeatureManagement'),
[Scene.FeatureManagementNew]: () => import('./feature-management/FeatureManagementEdit'),
[Scene.FeatureFlag]: () => import('./feature-flags/FeatureFlag'),
[Scene.EarlyAccessFeatures]: () => import('./early-access-features/EarlyAccessFeatures'),
[Scene.EarlyAccessFeature]: () => import('./early-access-features/EarlyAccessFeature'),
Expand Down
38 changes: 0 additions & 38 deletions frontend/src/scenes/feature-flags/FeatureManagement.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions frontend/src/scenes/feature-flags/featureManagementDetailLogic.ts

This file was deleted.

32 changes: 32 additions & 0 deletions frontend/src/scenes/feature-management/FeatureManagement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useValues } from 'kea'
import { SceneExport } from 'scenes/sceneTypes'

import { FeatureManagementDetail } from './FeatureManagementDetail'
import { FeatureManagementEmptyState } from './FeatureManagementEmptyState'
import { FeatureManagementList } from './FeatureManagementList'
import { featureManagementLogic } from './featureManagementLogic'

export const scene: SceneExport = {
component: FeatureManagement,
logic: featureManagementLogic,
}

export function FeatureManagement(): JSX.Element {
const { features } = useValues(featureManagementLogic)

if (features?.results.length === 0) {
return <FeatureManagementEmptyState />
}

return (
<div className="flex gap-4">
<div className="flex-none w-80">
<FeatureManagementList />
</div>

<div className="grow">
<FeatureManagementDetail />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import { LemonSkeleton } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { LemonButton, LemonDivider, LemonSkeleton } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { More } from 'lib/lemon-ui/LemonButton/More'

import { featureManagementDetailLogic } from './featureManagementDetailLogic'

function Header(): JSX.Element {
const { activeFeature } = useValues(featureManagementDetailLogic)
const { deleteFeature } = useActions(featureManagementDetailLogic)

return (
<div className="flex justify-between items-center">
<div className="text-xl font-bold">{activeFeature?.name}</div>
<More
overlay={
<>
<LemonButton fullWidth>Edit</LemonButton>
<LemonDivider />
<LemonButton status="danger" fullWidth onClick={() => deleteFeature(activeFeature)}>

Check failure on line 19 in frontend/src/scenes/feature-management/FeatureManagementDetail.tsx

View workflow job for this annotation

GitHub Actions / Code quality checks

Argument of type 'FeatureType | null' is not assignable to parameter of type 'FeatureType'.
Delete feature
</LemonButton>
</>
}
/>
</div>
)
}

function Metadata(): JSX.Element {
return (
<div className="flex flex-col gap-2">
Expand Down Expand Up @@ -70,11 +93,9 @@
}

export function FeatureManagementDetail(): JSX.Element {
const { activeFeature } = useValues(featureManagementDetailLogic)

return (
<div className="flex flex-col gap-16">
<div className="text-xl font-bold">{activeFeature?.name}</div>
<Header />
<Metadata />
<Rollout />
<Usage />
Expand Down
105 changes: 105 additions & 0 deletions frontend/src/scenes/feature-management/FeatureManagementEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { LemonButton, LemonDivider, LemonInput, LemonTextArea } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { Form } from 'kea-forms'
import { router } from 'kea-router'
import { PageHeader } from 'lib/components/PageHeader'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'

import { featureManagementEditLogic } from './featureManagementEditLogic'

export const scene: SceneExport = {
component: FeatureManagementEdit,
logic: featureManagementEditLogic,
paramsToProps: ({ params: { id } }): (typeof featureManagementEditLogic)['props'] => ({
id: id && id !== 'new' ? id : 'new',
}),
}

function FeatureManagementEdit(): JSX.Element {
const { props } = useValues(featureManagementEditLogic)

return (
<Form
id="feature-creation"
logic={featureManagementEditLogic}
props={props}
formKey="feature"
enableFormOnSubmit
className="space-y-4"
>
<PageHeader
buttons={
<div className="flex items-center gap-2">
<LemonButton
data-attr="cancel-feature-flag"
type="secondary"
onClick={() => router.actions.push(urls.featureManagement())}
>
Cancel
</LemonButton>
<LemonButton
type="primary"
data-attr="save-feature-flag"
htmlType="submit"
form="feature-creation"
>
Save
</LemonButton>
</div>
}
/>
<div className="my-4">
<div className="max-w-1/2 space-y-4">
<LemonField name="name" label="Name">
<LemonInput
data-attr="feature-name"
className="ph-ignore-input"
autoFocus
placeholder="examples: Login v2, New registration flow, Mobile web"
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
/>
</LemonField>

<LemonField name="key" label="Key">
<LemonInput
data-attr="feature-key"
className="ph-ignore-input"
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
disabled
/>
</LemonField>
<span className="text-muted text-sm">
This will be used to monitor feature usage. Feature keys must be unique to other features and
feature flags.
</span>

<LemonField name="description" label="Description">
<LemonTextArea className="ph-ignore-input" data-attr="feature-description" />
</LemonField>
</div>
</div>
<LemonDivider />

<div className="flex items-center gap-2 justify-end">
<LemonButton
data-attr="cancel-feature-flag"
type="secondary"
onClick={() => router.actions.push(urls.featureManagement())}
>
Cancel
</LemonButton>
<LemonButton type="primary" data-attr="save-feature-flag" htmlType="submit" form="feature-creation">
Save
</LemonButton>
</div>
</Form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IconPlusSmall } from '@posthog/icons'
import { BuilderHog3 } from 'lib/components/hedgehogs'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { Link } from 'lib/lemon-ui/Link'
import { urls } from 'scenes/urls'

export function FeatureManagementEmptyState(): JSX.Element {
return (
<div className="text-center" data-attr="feature-flag-empty-state-filtered">
<div className="w-40 m-auto">
<BuilderHog3 className="w-full h-full" />
</div>
<h2>No features created yet</h2>
<p>Start your first big feature rollout today.</p>

<div className="flex justify-center">
<Link to={urls.featureManagementNew()}>
<LemonButton type="primary" data-attr="empty-state-add-feature-button" icon={<IconPlusSmall />}>
New feature
</LemonButton>
</Link>
</div>
</div>
)
}
50 changes: 50 additions & 0 deletions frontend/src/scenes/feature-management/FeatureManagementList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { IconPlusSmall } from '@posthog/icons'
import { LemonSkeleton, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { urls } from 'scenes/urls'

import { featureManagementLogic } from './featureManagementLogic'

export function FeatureManagementList(): JSX.Element {
const { activeFeatureId, features, featuresLoading } = useValues(featureManagementLogic)
const { setActiveFeatureId } = useActions(featureManagementLogic)

const header = (
<div className="flex align-middle justify-between">
<h2>Features</h2>
<Link to={urls.featureManagementNew()}>
<LemonButton type="primary" data-attr="add-feature-button" icon={<IconPlusSmall />}>
New feature
</LemonButton>
</Link>
</div>
)

return (
<div className="flex flex-col gap-4">
{header}
<div className="flex flex-col gap-1">
{featuresLoading && (
<>
<LemonSkeleton className="w-full h-8" active />
<LemonSkeleton className="w-full h-8" active />
<LemonSkeleton className="w-full h-8" active />
</>
)}
{features?.results.map((feature) => (
<div key={feature.id}>
<LemonButton
onClick={() => setActiveFeatureId(feature.id)}
size="small"
fullWidth
active={activeFeatureId === feature.id}
>
<span className="truncate">{feature.name}</span>
</LemonButton>
</div>
))}
</div>
</div>
)
}
Loading
Loading