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: Implements feature healthcare UI #5043

Merged
merged 8 commits into from
Jan 30, 2025
Merged
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
address changes and creates provider
tiagoapolo committed Jan 30, 2025
commit 14c577ad0ddc6bcb5d259bdcbd57ade1c8a4818a
70 changes: 70 additions & 0 deletions frontend/common/services/useHealthProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const healthProviderService = service
.enhanceEndpoints({ addTagTypes: ['HealthProviders'] })
.injectEndpoints({
endpoints: (builder) => ({
createHealthProvider: builder.mutation<
Res['healthProvider'],
Req['createHealthProvider']
>({
invalidatesTags: [{ id: 'LIST', type: 'HealthProviders' }],
query: (query: Req['createHealthProvider']) => ({
body: { name: query.name },
method: 'POST',
url: `projects/${query.projectId}/feature-health/providers/`,
}),
}),
getHealthProviders: builder.query<
Res['healthProviders'],
Req['getHealthProviders']
>({
providesTags: [{ id: 'LIST', type: 'HealthProviders' }],
query: (query: Req['getHealthProviders']) => ({
url: `projects/${query.projectId}/feature-health/providers/`,
}),
}),
// END OF ENDPOINTS
}),
})

export async function getHealthProviders(
store: any,
data: Req['getHealthProviders'],
options?: Parameters<
typeof healthProviderService.endpoints.getHealthProviders.initiate
>[1],
) {
return store.dispatch(
healthProviderService.endpoints.getHealthProviders.initiate(data, options),
)
}

export async function createHealthProvider(
store: any,
data: Req['createHealthProvider'],
options?: Parameters<
typeof healthProviderService.endpoints.createHealthProvider.initiate
>[1],
) {
return store.dispatch(
healthProviderService.endpoints.createHealthProvider.initiate(
data,
options,
),
)
}
// END OF FUNCTION_EXPORTS

export const {
useCreateHealthProviderMutation,
useGetHealthProvidersQuery,
// END OF EXPORTS
} = healthProviderService

/* Usage examples:
const { data, isLoading } = useGetHealthProvidersQuery({ id: 2 }, {}) //get hook
healthProviderService.endpoints.getHealthProviders.select({id: 2})(store.getState()) //access data from any function
*/
6 changes: 1 addition & 5 deletions frontend/common/services/useTag.ts
Original file line number Diff line number Diff line change
@@ -68,11 +68,7 @@ export async function getTags(
data: Req['getTags'],
options?: Parameters<typeof tagService.endpoints.getTags.initiate>[1],
) {
const result = await store.dispatch(
tagService.endpoints.getTags.initiate(data, options),
)
await Promise.all(store.dispatch(tagService.util.getRunningQueriesThunk()))
return result
return store.dispatch(tagService.endpoints.getTags.initiate(data, options))
}
export async function createTag(
store: any,
2 changes: 2 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
@@ -115,6 +115,8 @@ export type Req = {
getAvailablePermissions: { level: PermissionLevel }
getTag: { id: string }
getHealthEvents: { projectId: number | string }
getHealthProviders: { projectId: number }
createHealthProvider: { projectId: number; name: string }
updateTag: { projectId: string; tag: Tag }
deleteTag: {
id: number
11 changes: 10 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
@@ -352,7 +352,7 @@ export type Tag = {
label: string
is_system_tag: boolean
is_permanent: boolean
type: 'STALE' | 'NONE'
type: 'STALE' | 'UNHEALTHY' | 'NONE'
}

export type MultivariateFeatureStateValue = {
@@ -646,6 +646,13 @@ export type HealthEvent = {
type: 'HEALTHY' | 'UNHEALTHY'
}

export type HealthProvider = {
created_by: string
name: string
project: number
webhook_url: number
}

export type Res = {
segments: PagedResponse<Segment>
segment: Segment
@@ -681,6 +688,8 @@ export type Res = {
tag: Tag
tags: Tag[]
healthEvents: HealthEvent[]
healthProvider: HealthProvider
healthProviders: HealthProvider[]
account: Account
userEmail: {}
groupAdmin: { id: string }
237 changes: 237 additions & 0 deletions frontend/web/components/EditHealthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React, { FC } from 'react'
import {
HealthProvider,
Role,
User,
UserGroupSummary,
UserPermission,
} from 'common/types/responses'
import PanelSearch from './PanelSearch'
import Button from './base/forms/Button'

import { PermissionLevel, Req } from 'common/types/requests'
import { RouterChildContext } from 'react-router'

import ConfigProvider from 'common/providers/ConfigProvider'
import Icon from './Icon'

import Utils from 'common/utils/utils'
import {
useCreateHealthProviderMutation,
useGetHealthProvidersQuery,
} from 'common/services/useHealthProvider'
import { components } from 'react-select'

type EditPermissionModalType = {
group?: UserGroupSummary
projectId: number
className?: string
isGroup?: boolean
level: PermissionLevel
name: string
onSave?: () => void
envId?: number | string | undefined
parentId?: string
parentLevel?: string
parentSettingsLink?: string
roleTabTitle?: string
permissions?: UserPermission[]
push: (route: string) => void
user?: User
role?: Role
roles?: Role[]
permissionChanged?: () => void
isEditUserPermission?: boolean
isEditGroupPermission?: boolean
}

type EditHealthProviderType = Omit<EditPermissionModalType, 'onSave'> & {
router: RouterChildContext['router']
tabClassName?: string
}

const CreateHealthProviderForm = ({ projectId }: { projectId: number }) => {
const [selected, setSelected] = React.useState<string | undefined>()
const [createProvider, { isError, isLoading, isSuccess }] =
useCreateHealthProviderMutation()

const providers = [{ name: 'Sample' }, { name: 'Grafana' }]

const providerOptions = providers.map((provider) => ({
label: provider.name,
value: provider.name,
}))

return (
<form
className='col-md-8'
onSubmit={(e) => {
e.preventDefault()
if (!selected) {
return
}
createProvider({ name: selected, projectId })
}}
>
<Row className='align-items-start'>
<Flex className='ml-0'>
<Select
disabled={!providerOptions?.length}
placeholder='Select a provider'
data-test='add-health-provider-select'
components={{
Option: (props: any) => {
return (
<components.Option {...props}>
{props.children}
</components.Option>
)
},
}}
value={providerOptions.find((v) => v.value === selected)}
onChange={(option: { value: string }) => {
setSelected(option.value)
}}
options={providerOptions}
/>
</Flex>
</Row>
<div className='text-right mt-4'>
<Button
type='submit'
id='save-proj-btn'
disabled={isLoading || !selected}
className='ml-3'
>
{isLoading ? 'Creating' : 'Create'}
</Button>
</div>
</form>
)
}

const EditHealthProvider: FC<EditHealthProviderType> = ({
envId,
level,
permissions,
projectId,
roleTabTitle,
roles,
router,
tabClassName,
}) => {
const { data: healthProviders, isLoading } = useGetHealthProvidersQuery({
projectId,
})

return (
<div className='mt-4'>
<Row>
<h5>Create Health Providers</h5>
</Row>
<p className='fs-small lh-sm col-md-8 mb-4'>
Flagsmith lets you connect health providers for tagging feature flags
unhealthy state in different environments.{' '}
<Button
theme='text'
href='' // TODO: Add docs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add documentation for feature health usage

target='_blank'
className='fw-normal'
>
Learn about Feature Health.
</Button>
</p>

<label>Provider Name</label>
<CreateHealthProviderForm projectId={projectId} />
<hr className='py-0 my-4' />

<div className='mt-4'>
{isLoading && (
<div className='centered-container'>
<Loader />
</div>
)}
{!isLoading && !!healthProviders?.length && (
<div className={tabClassName}>
<PanelSearch
id='project-health-providers-list'
title='Health Providers'
className='panel--transparent'
items={healthProviders}
itemHeight={64}
header={
<Row className='table-header'>
<Flex className='table-column px-3'>Provider</Flex>
<Flex className='table-column'>Webhook URL</Flex>
</Row>
}
renderRow={(provider: HealthProvider) => {
const { name, webhook_url: webhook } = provider
const matchingPermissions = {
admin: true,
}

return (
<Row
space
className={`list-item${
matchingPermissions?.admin ? '' : ' clickable'
}`}
key={projectId}
>
<Flex className='table-column px-3'>
<div className='mb-1 font-weight-medium'>{name}</div>
</Flex>
{matchingPermissions?.admin && (
<Flex className='table-column fs-small lh-sm'>
<div className='d-flex align-items-center'>
<div
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '280px',
}}
>
{webhook}
</div>
<Button
onClick={() => {
Utils.copyFeatureName(webhook)
}}
theme='icon'
className='ms-2'
>
<Icon name='copy' />
</Button>
</div>
</Flex>
)}
<div style={{ width: '80px' }} className='text-center'>
{matchingPermissions?.admin && (
<Icon name='setting' width={20} fill='#656D7B' />
)}
</div>
</Row>
)
}}
renderNoResults={
<div>You have no health provider in this project.</div>
}
filterRow={(item: HealthProvider, search: string) => {
const strToSearch = `${item.name} ${item.webhook_url}`
return (
strToSearch.toLowerCase().indexOf(search.toLowerCase()) !== -1
)
}}
/>
</div>
)}
</div>
</div>
)
}

export default ConfigProvider(EditHealthProvider) as unknown as FC<
Omit<EditHealthProviderType, 'router'>
>
Loading