Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 0 additions & 1 deletion frontend/common/dispatcher/action-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), {
'ENABLE_TWO_FACTOR': 'ENABLE_TWO_FACTOR',
'GET_CHANGE_REQUEST': 'GET_CHANGE_REQUEST',
'GET_ENVIRONMENT': 'GET_ENVIRONMENT',
'GET_FEATURE_USAGE': 'GET_FEATURE_USAGE',
'GET_FLAGS': 'GET_FLAGS',
'GET_IDENTITY': 'GET_IDENTITY',
'GET_ORGANISATION': 'GET_ORGANISATION',
Expand Down
9 changes: 0 additions & 9 deletions frontend/common/dispatcher/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), {
projectId,
})
},
getFeatureUsage(projectId, environmentId, flag, period) {
Dispatcher.handleViewAction({
actionType: Actions.GET_FEATURE_USAGE,
environmentId,
flag,
period,
projectId,
})
},
getFeatures(
projectId,
environmentId,
Expand Down
2 changes: 0 additions & 2 deletions frontend/common/providers/FeatureListProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const FeatureListProvider = class extends React.Component {
maxFeaturesAllowed: ProjectStore.getMaxFeaturesAllowed(),
projectFlags: FeatureListStore.getProjectFlags(),
totalFeatures: ProjectStore.getTotalFeatures(),
usageData: FeatureListStore.getFeatureUsage(),
})
})
this.listenTo(FeatureListStore, 'removed', (data) => {
Expand All @@ -44,7 +43,6 @@ const FeatureListProvider = class extends React.Component {
isLoading: FeatureListStore.isLoading,
isSaving: FeatureListStore.isSaving,
lastSaved: FeatureListStore.getLastSaved(),
usageData: FeatureListStore.getFeatureUsage(),
})
this.props.onError && this.props.onError(FeatureListStore.error)
})
Expand Down
90 changes: 90 additions & 0 deletions frontend/common/services/useFeatureAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import { sortBy } from 'lodash'
import moment from 'moment'
import range from 'lodash/range'

export const featureAnalyticsService = service
.enhanceEndpoints({ addTagTypes: ['FeatureAnalytics'] })
.injectEndpoints({
endpoints: (builder) => ({
getFeatureAnalytics: builder.query<
Res['featureAnalytics'],
Req['getFeatureAnalytics']
>({
providesTags: [{ id: 'LIST', type: 'FeatureAnalytics' }],
queryFn: async (query, baseQueryApi, extraOptions, baseQuery) => {
const responses = await Promise.all(
query.environment_ids.map((environment_id) => {
return baseQuery({
url: `projects/${query.project_id}/features/${query.feature_id}/evaluation-data/?period=${query.period}&environment_id=${environment_id}`,
})
}),
)

const error = responses.find((v) => !!v.error)?.error
const today = moment().startOf('day')
const startDate = moment(today).subtract(query.period - 1, 'days')
const preBuiltData: Res['featureAnalytics'] = []
for (
let date = startDate.clone();
date.isSameOrBefore(today);
date.add(1, 'days')
) {
const dayObj: Res['featureAnalytics'][number] = {
day: date.format('Do MMM'),
}
query.environment_ids.forEach((envId) => {
dayObj[envId] = 0
})
preBuiltData.push(dayObj)
}

responses.forEach((response, i) => {
const environment_id = query.environment_ids[i]

response.data.forEach((entry: Res['featureAnalytics'][number]) => {
const date = moment(entry.day).format('Do MMM')
const dayEntry = preBuiltData.find((d) => d.day === date)
if (dayEntry) {
dayEntry[environment_id] = entry.count // Set count for specific environment ID
}
})
})
return {
data: error ? [] : preBuiltData,
error,
}
},
}),
// END OF ENDPOINTS
}),
})

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

export const {
useGetFeatureAnalyticsQuery,
// END OF EXPORTS
} = featureAnalyticsService

/* Usage examples:
const { data, isLoading } = useGetFeatureAnalyticsQuery({ id: 2 }, {}) //get hook
const [createFeatureAnalytics, { isLoading, data, isSuccess }] = useCreateFeatureAnalyticsMutation() //create hook
featureAnalyticsService.endpoints.getFeatureAnalytics.select({id: 2})(store.getState()) //access data from any function
*/
6 changes: 6 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,5 +783,11 @@ export type Req = {
pipelineId: number
name: string
}
getFeatureAnalytics: {
project_id: string
feature_id: string
period: number
environment_ids: string[]
}
// END OF TYPES
}
4 changes: 4 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1107,5 +1107,9 @@ export type Res = {
releasePipeline: SingleReleasePipeline
pipelineStages: PagedResponse<PipelineStage>
featureCodeReferences: FeatureCodeReferences[]
featureAnalytics: {
day: string
[environmentId: string]: string | number
}[]
// END OF TYPES
}
1 change: 0 additions & 1 deletion frontend/web/components/AuditLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ const AuditLog: FC<AuditLogType> = (props) => {
color: Utils.getTagColour(colour),
label: environment?.name,
}}
className='chip--sm'
/>
</Link>
) : (
Expand Down
123 changes: 123 additions & 0 deletions frontend/web/components/EnvironmentTagSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { FC, useMemo } from 'react'
import { useGetEnvironmentsQuery } from 'common/services/useEnvironment'
import { Props } from 'react-select/lib/Select'
import Tag from './tags/Tag'
import Utils from 'common/utils/utils'
import Button from './base/forms/Button'

export type EnvironmentSelectType = Omit<
Partial<Props>,
'value' | 'onChange'
> & {
projectId: string | number | undefined
value?: string[] | string | null
onChange: (value: string[] | string | undefined) => void
idField?: 'id' | 'api_key'
dataTest?: (value: { label: string }) => string
multiple?: boolean
allowEmpty?: boolean
}

const EnvironmentSelect: FC<EnvironmentSelectType> = ({
allowEmpty = false,
idField = 'api_key',
ignore,
multiple = false,
onChange,
projectId,
value,
}) => {
const { data } = useGetEnvironmentsQuery({ projectId: `${projectId}` })
const environments = useMemo(() => {
return (data?.results || [])
?.map((v) => ({
label: v.name,
value: `${v[idField]}`,
}))
.filter((v) => {
if (ignore) {
return !ignore.includes(v.value)
}
return true
})
}, [data?.results, ignore, idField])

const handleSelectAll = () => {
if (multiple) {
onChange(environments.map((env) => env.value))
}
}

const handleClearAll = () => {
if (multiple) {
onChange(allowEmpty ? [] : undefined)
}
}

const selectedCount = Array.isArray(value) ? value.length : 0
const allSelected = selectedCount === environments.length

return (
<div className='d-flex align-items-center mb-2'>
<Row className='flex-1 row-gap-1'>
{environments.map((env, i) => (
<Tag
tag={{
color: Utils.getTagColour(i),
label: env.label,
}}
key={env.value}
selected={
multiple
? Array.isArray(value) && value.includes(env.value)
: value === env.value
}
onClick={() => {
if (multiple) {
if (Array.isArray(value) && value.includes(env.value)) {
const newValue = value.filter((v) => v !== env.value)
onChange(allowEmpty && newValue.length === 0 ? [] : newValue)
} else {
onChange((value || []).concat([env.value]))
}
} else {
onChange(
value === env.value
? allowEmpty
? undefined
: value
: env.value,
)
}
}}
className='mb-2 chip--xs'
/>
))}
</Row>
{multiple && environments.length > 0 && (
<div
className='flex-shrink-0 ml-2 text-right'
style={{ width: '140px' }}
>
{!allSelected && (
<Button
onClick={handleSelectAll}
size='xSmall'
theme='text'
className='mr-2'
>
Select All
</Button>
)}
{selectedCount > 1 && (
<Button onClick={handleClearAll} size='xSmall' theme='text'>
Clear
</Button>
)}
</div>
)}
</div>
)
}

export default EnvironmentSelect
40 changes: 0 additions & 40 deletions frontend/web/components/ToggleChip.js

This file was deleted.

51 changes: 51 additions & 0 deletions frontend/web/components/ToggleChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { FC, ReactNode } from 'react'
import cx from 'classnames'
import Icon from './Icon'
import Utils from 'common/utils/utils'

export type ToggleChipProps = {
color?: string
active?: boolean
onClick?: () => void
className?: string
children?: ReactNode
}

const ToggleChip: FC<ToggleChipProps> = ({
active,
children,
className,
color,
onClick,
}) => {
const colour = Utils.colour(color)
return (
<Row
style={
color
? {
backgroundColor: children ? colour.fade(0.92) : colour.fade(0.76),
border: `1px solid ${colour.fade(0.76)}`,
color: colour.darken(0.1),
}
: undefined
}
onClick={onClick}
className={cx('chip no-wrap mr-1 mt-0 clickable', className)}
>
<span
style={{
backgroundColor: active ? 'white' : 'transparent',
border:
active || !children ? 'none' : `1px solid ${colour.fade(0.76)}`,
}}
className={cx('icon-check', children ? 'mr-2' : null)}
>
{active && <Icon name='checkmark-square' fill={color} />}
</span>
{children}
</Row>
)
}

export default ToggleChip
Loading
Loading