diff --git a/frontend/common/dispatcher/action-constants.js b/frontend/common/dispatcher/action-constants.js index 87ef249fad24..0ae955729756 100644 --- a/frontend/common/dispatcher/action-constants.js +++ b/frontend/common/dispatcher/action-constants.js @@ -29,7 +29,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), { 'GET_CHANGE_REQUEST': 'GET_CHANGE_REQUEST', 'GET_CHANGE_REQUESTS': 'GET_CHANGE_REQUESTS', 'GET_ENVIRONMENT': 'GET_ENVIRONMENT', - 'GET_FEATURE_USAGE': 'GET_FEATURE_USAGE', 'GET_FLAGS': 'GET_FLAGS', 'GET_IDENTITY': 'GET_IDENTITY', 'GET_IDENTITY_SEGMENTS': 'GET_IDENTITY_SEGMENTS', diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index 0eab0e1d1245..da52f1553af2 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -222,15 +222,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { page, }) }, - getFeatureUsage(projectId, environmentId, flag, period) { - Dispatcher.handleViewAction({ - actionType: Actions.GET_FEATURE_USAGE, - environmentId, - flag, - period, - projectId, - }) - }, getFeatures( projectId, environmentId, diff --git a/frontend/common/providers/FeatureListProvider.js b/frontend/common/providers/FeatureListProvider.js index 3b8738bcf141..a655c37ba610 100644 --- a/frontend/common/providers/FeatureListProvider.js +++ b/frontend/common/providers/FeatureListProvider.js @@ -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) => { @@ -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) }) diff --git a/frontend/common/services/useFeatureAnalytics.ts b/frontend/common/services/useFeatureAnalytics.ts new file mode 100644 index 000000000000..dd92360ef540 --- /dev/null +++ b/frontend/common/services/useFeatureAnalytics.ts @@ -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 featureAnalyticService = service + .enhanceEndpoints({ addTagTypes: ['FeatureAnalytic'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + getFeatureAnalytics: builder.query< + Res['featureAnalytics'], + Req['getFeatureAnalytics'] + >({ + providesTags: [{ id: 'LIST', type: 'FeatureAnalytic' }], + 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 featureAnalyticService.endpoints.getFeatureAnalytics.initiate + >[1], +) { + return store.dispatch( + featureAnalyticService.endpoints.getFeatureAnalytics.initiate( + data, + options, + ), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useGetFeatureAnalyticsQuery, + // END OF EXPORTS +} = featureAnalyticService + +/* Usage examples: +const { data, isLoading } = useGetFeatureAnalyticsQuery({ id: 2 }, {}) //get hook +const [createFeatureAnalytics, { isLoading, data, isSuccess }] = useCreateFeatureAnalyticsMutation() //create hook +featureAnalyticService.endpoints.getFeatureAnalytics.select({id: 2})(store.getState()) //access data from any function +*/ diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index 83aec0b5cfb1..54a4eb468f3c 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -8,7 +8,7 @@ import dataRelay from 'data-relay' import { sortBy } from 'lodash' import Project from 'common/project' import { getStore } from 'common/store' -import { service } from "common/service"; +import { service } from 'common/service' const controller = { acceptInvite: (id) => { diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index ebc4fa9ccc92..48a124d61b43 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -12,12 +12,13 @@ import { } from 'common/services/useProjectFlag' import OrganisationStore from './organisation-store' import { - ChangeRequest, - Environment, - FeatureState, - PagedResponse, - ProjectFlag, TypedFeatureState, -} from 'common/types/responses'; + ChangeRequest, + Environment, + FeatureState, + PagedResponse, + ProjectFlag, + TypedFeatureState, +} from 'common/types/responses' import Utils from 'common/utils/utils' import Actions from 'common/dispatcher/action-constants' import Project from 'common/project' @@ -472,16 +473,17 @@ const controller = { API.trackEvent(Constants.events.EDIT_FEATURE) const env: Environment = ProjectStore.getEnvironment(environmentId) as any // Detect differences between change request and existing feature states - const res: { data: PagedResponse } = await getFeatureStates( - getStore(), - { - environment: environmentFlag.environment, - feature: projectFlag.id, - }, - { - forceRefetch: true, - }, - ) + const res: { data: PagedResponse } = + await getFeatureStates( + getStore(), + { + environment: environmentFlag.environment, + feature: projectFlag.id, + }, + { + forceRefetch: true, + }, + ) const segmentResult = await getSegments(getStore(), { include_feature_specific: true, page_size: 1000, @@ -800,42 +802,6 @@ const controller = { API.ajaxHandler(store, e) }) }, - getFeatureUsage(projectId, environmentId, flag, period) { - data - .get( - `${Project.api}projects/${projectId}/features/${flag}/evaluation-data/?period=${period}&environment_id=${environmentId}`, - ) - .then((result) => { - const firstResult = result[0] - const lastResult = firstResult && result[result.length - 1] - const diff = firstResult - ? moment(lastResult.day, 'YYYY-MM-DD').diff( - moment(firstResult.day, 'YYYY-MM-DD'), - 'days', - ) - : 0 - if (firstResult && diff) { - _.range(0, diff).map((v) => { - const day = moment(firstResult.day) - .add(v, 'days') - .format('YYYY-MM-DD') - if (!result.find((v) => v.day === day)) { - result.push({ - 'count': 0, - day, - }) - } - }) - } - store.model.usageData = _.sortBy(result, (v) => - moment(v.day, 'YYYY-MM-DD').valueOf(), - ).map((v) => ({ - ...v, - day: moment(v.day, 'YYYY-MM-DD').format('Do MMM'), - })) - store.changed() - }) - }, getFeatures: (projectId, environmentId, force, page, filter, pageSize) => { if (!store.model || store.envId !== environmentId || force) { store.envId = environmentId @@ -975,9 +941,6 @@ const store = Object.assign({}, BaseStore, { getEnvironmentFlags() { return store.model && store.model.keyedEnvironmentFeatures }, - getFeatureUsage() { - return store.model && store.model.usageData - }, getLastSaved() { return store.model && store.model.lastSaved }, @@ -1041,14 +1004,6 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { ) } break - case Actions.GET_FEATURE_USAGE: - controller.getFeatureUsage( - action.projectId, - action.environmentId, - action.flag, - action.period, - ) - break case Actions.CREATE_FLAG: controller.createFlag(action.projectId, action.environmentId, action.flag) break diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index ab22c7ac00ee..c47be9fce618 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -211,6 +211,7 @@ export type Req = { environmentId?: string tags?: string[] is_archived?: boolean + search?: string } getProjectFlag: { project: string | number; id: string } getRolesPermissionUsers: { organisation_id: number; role_id: number } @@ -540,5 +541,11 @@ export type Req = { environmentId: string data: Identity } + getFeatureAnalytics: { + project_id: string + feature_id: string + period: number + environment_ids: string[] + } // END OF TYPES } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index ea8c3d03c246..8484fb1f9498 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -784,5 +784,9 @@ export type Res = { metadata_xml: string } samlAttributeMapping: PagedResponse + featureAnalytics: { + day: string + [environmentId: string]: string | number | undefined // Dynamic properties for environment IDs + }[] // END OF TYPES } diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 32cf99123e76..b1dfb357185c 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -37,6 +37,7 @@ import HomeAside from './pages/HomeAside' import ScrollToTop from './ScrollToTop' import AnnouncementPerPage from './AnnouncementPerPage' import Announcement from './Announcement' +import AccountProvider from 'common/providers/AccountProvider' const App = class extends Component { static propTypes = { @@ -216,9 +217,7 @@ const App = class extends Component { this.context.router.history.replace(redirect) } else { AsyncStorage.getItem('lastEnv').then((res) => { - if ( - this.props.location.search.includes('github-redirect') - ) { + if (this.props.location.search.includes('github-redirect')) { this.context.router.history.replace( `/github-setup${this.props.location.search}`, ) @@ -594,6 +593,15 @@ const App = class extends Component { > Compare + {!Project.disableAnalytics && ( + } + id='feature-analytics-link' + to={`/project/${projectId}/feature-analytics`} + > + Feature Analytics + + )} } id='org-settings-link' - data-test='org-settings-link' + data-test='org-settings-link' to={`/organisation/${ AccountStore.getOrganisation().id }/settings`} diff --git a/frontend/web/components/CompareFeatures.js b/frontend/web/components/CompareFeatures.js index 5bf1c1082b06..a6ba6f584010 100644 --- a/frontend/web/components/CompareFeatures.js +++ b/frontend/web/components/CompareFeatures.js @@ -1,6 +1,6 @@ // import propTypes from 'prop-types'; import React, { Component } from 'react' -import FlagSelect from './FlagSelect' +import FeatureSelect from './FeatureSelect' import ProjectStore from 'common/stores/project-store' import data from 'common/data/base/_data' import FeatureRow from './FeatureRow' @@ -78,7 +78,7 @@ class CompareEnvironments extends Component {
- diff --git a/frontend/web/components/EnvironmentTagSelect.tsx b/frontend/web/components/EnvironmentTagSelect.tsx new file mode 100644 index 000000000000..bc868a72202a --- /dev/null +++ b/frontend/web/components/EnvironmentTagSelect.tsx @@ -0,0 +1,83 @@ +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' + +export type EnvironmentSelectType = Omit< + Partial, + 'value' | 'onChange' +> & { + projectId: string + 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 = ({ + 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]) + + return ( + + {environments.map((env, i) => ( + { + 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='mr-2 mb-2' + /> + ))} + + ) +} + +export default EnvironmentSelect diff --git a/frontend/web/components/FeatureAction.tsx b/frontend/web/components/FeatureAction.tsx index 8dcbff94d281..5d7adf8c7ba5 100644 --- a/frontend/web/components/FeatureAction.tsx +++ b/frontend/web/components/FeatureAction.tsx @@ -21,13 +21,12 @@ interface FeatureActionProps { hideHistory: boolean hideRemove: boolean onShowHistory: () => void - isCompact?: boolean - onCopyName: () => void + onShowAnalytics: () => void onShowAudit: () => void onRemove: () => void } -type ActionType = 'copy' | 'audit' | 'history' | 'remove' +type ActionType = 'analytics' | 'audit' | 'history' | 'remove' function calculateListPosition( btnEl: HTMLElement, @@ -47,9 +46,8 @@ export const FeatureAction: FC = ({ hideAudit, hideHistory, hideRemove, - isCompact, - onCopyName, onRemove, + onShowAnalytics, onShowAudit, onShowHistory, projectId, @@ -71,19 +69,19 @@ export const FeatureAction: FC = ({ const handleActionClick = useCallback( (action: ActionType) => { - if (action === 'copy') { - onCopyName() - } else if (action === 'history') { + if (action === 'history') { onShowHistory() } else if (action === 'audit') { onShowAudit() } else if (action === 'remove') { onRemove() + } else if (action === 'analytics') { + onShowAnalytics() } close() }, - [close, onCopyName, onRemove, onShowHistory], + [close, onShowAudit, onShowAnalytics, onRemove, onShowHistory], ) useOutsideClick(listRef, handleOutsideClick) @@ -109,13 +107,14 @@ export const FeatureAction: FC = ({
{ e.stopPropagation() - handleActionClick('copy') + handleActionClick('analytics') }} > - - Copy Feature Name + + Analytics
{!hideAudit && (
= ({ }} > - Show Audit Logs + Audit Logs
)} - {!hideHistory && (
= ({ }} > - Show History + History
)} @@ -156,42 +154,51 @@ export const FeatureAction: FC = ({ Utils.renderWithPermission( removeFeaturePermission, Constants.projectPermissions('Delete Feature'), - { - e.stopPropagation() - handleActionClick('remove') - }} - > - - Remove feature -
- } - > - {isProtected && - `This feature has been tagged with the permanent tag${ - protectedTags?.length > 1 ? 's' : '' - } ${protectedTags - ?.map((tag) => { - const tagColor = Utils.colour(getTagColor(tag)) - return ` +
+
+ { + e.stopPropagation() + handleActionClick('remove') + }} + > + + Remove feature +
+ } + > + {isProtected + ? `This feature has been tagged with the permanent tag${ + protectedTags?.length > 1 ? 's' : '' + } ${protectedTags + ?.map((tag) => { + const tagColor = Utils.colour(getTagColor(tag)) + return ` ${tag.label} ` - }) - .join('')}. Please remove the tag${ - protectedTags?.length > 1 ? 's' : '' - } before attempting to delete this flag.`} - , + }) + .join('')}. Please remove the tag${ + protectedTags?.length > 1 ? 's' : '' + } before attempting to delete this flag.
` + : ''} + +
, ) }
diff --git a/frontend/web/components/FeatureAnalytics.tsx b/frontend/web/components/FeatureAnalytics.tsx new file mode 100644 index 000000000000..cc22e3f41653 --- /dev/null +++ b/frontend/web/components/FeatureAnalytics.tsx @@ -0,0 +1,119 @@ +import React, { FC, useState } from 'react' +import { sortBy } from 'lodash' +import Color from 'color' +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' +import InfoMessage from './InfoMessage' +import EnvironmentTagSelect from './EnvironmentTagSelect' +import { useGetFeatureAnalyticsQuery } from 'common/services/useFeatureAnalytics' +import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' +import Utils from 'common/utils/utils' + +type FlagAnalyticsType = { + projectId: string + featureId: string + defaultEnvironmentIds: string[] +} + +const FlagAnalytics: FC = ({ + defaultEnvironmentIds, + featureId, + projectId, +}) => { + const [environmentIds, setEnvironmentIds] = useState(defaultEnvironmentIds) + const { data, isLoading } = useGetFeatureAnalyticsQuery( + { + environment_ids: environmentIds, + feature_id: featureId, + period: 30, + project_id: projectId, + }, + { + skip: !environmentIds?.length || !featureId || !projectId, + }, + ) + const { data: environments } = useGetEnvironmentsQuery({ + projectId: `${projectId}`, + }) + + return ( + <> + +
Flag events for last 30 days
+ + {isLoading && ( +
+ +
+ )} + {data && Array.isArray(data) && data.length > 0 && ( +
+ + + + + + + {sortBy(environmentIds, (id) => + environments?.results?.findIndex((env) => `${env.id}` === id), + ).map((id) => { + let index = environments?.results.findIndex( + (env) => `${env.id}` === id, + ) + if (index === -1) index = 0 + return ( + + ) + })} + + +
+ )} +
+ + The Flag Analytics data will be visible in the Dashboard between 30 + minutes and 1 hour after it has been collected.{' '} + + View docs + + + + ) +} + +export default FlagAnalytics diff --git a/frontend/web/components/FeatureRow.js b/frontend/web/components/FeatureRow.js index 53fb6f4da3e7..5ee26e9f8755 100644 --- a/frontend/web/components/FeatureRow.js +++ b/frontend/web/components/FeatureRow.js @@ -436,7 +436,12 @@ class TheComponent extends Component { removeFlag(projectId, projectFlag) }) }} - onCopyName={this.copyFeature} + onShowAnalytics={() => { + if (disableControls) return + this.context.router.history.push( + `/project/${projectId}/feature-analytics?env=${environment.id}&feature=${projectFlag.id}`, + ) + }} /> diff --git a/frontend/web/components/FeatureSelect.tsx b/frontend/web/components/FeatureSelect.tsx new file mode 100644 index 000000000000..ecb009dd1388 --- /dev/null +++ b/frontend/web/components/FeatureSelect.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from 'react' +import sortBy from 'lodash/sortBy' +import { ProjectFlag } from 'common/types/responses' +import { useGetProjectFlagsQuery } from 'common/services/useProjectFlag' + +type OptionType = { + flag: ProjectFlag + label: string + value: number +} + +type FeatureSelectProps = { + autoSelectFirst?: boolean + disabled?: boolean + ignore?: number[] + onChange: (value: number, flag: ProjectFlag) => void + onlyInclude?: number + placeholder?: string + projectId: string + value?: number | string +} + +const FeatureSelect = ({ + autoSelectFirst, + disabled, + ignore = [], + onChange, + onlyInclude, + placeholder, + projectId, + value, +}: FeatureSelectProps): JSX.Element => { + const [search, setSearch] = useState('') + const { data, isLoading } = useGetProjectFlagsQuery( + { project: projectId, search }, + { skip: !projectId }, + ) + + useEffect(() => { + if (data?.results?.length && autoSelectFirst && !value) { + const flag = sortBy(data?.results, 'name')[0] + onChange(flag.id, flag) + } + }, [data, autoSelectFirst, value]) + + if (!data || isLoading) { + return ( +
+ {/* Use your Loader component or any loading indicator */} + +
+ ) + } + + const options: OptionType[] = sortBy( + (data.results || []) + .map((feature) => ({ + flag: feature, + label: feature.name, + value: feature.id, + })) + .filter((opt) => !ignore.includes(opt.value)) + .filter((opt) => { + if (onlyInclude) { + return opt.value === onlyInclude + } + return true + }), + (o) => o.label, + ) + + return ( + v.value === this.props.value) - : null - } - isDisabled={this.props.disabled} - onInputChange={this.search} - placeholder={this.props.placeholder} - onChange={(v) => this.props.onChange(v.value, v.flag)} - options={options} - /> - ) - } -} - -export default FlagSelect diff --git a/frontend/web/components/modals/AssociatedSegmentOverrides.js b/frontend/web/components/modals/AssociatedSegmentOverrides.js index 8a5b17c4bbba..426859de9553 100644 --- a/frontend/web/components/modals/AssociatedSegmentOverrides.js +++ b/frontend/web/components/modals/AssociatedSegmentOverrides.js @@ -6,7 +6,7 @@ import withSegmentOverrides from 'common/providers/withSegmentOverrides' import FeatureListStore from 'common/stores/feature-list-store' import ConfigProvider from 'common/providers/ConfigProvider' import SegmentOverrides from 'components/SegmentOverrides' -import FlagSelect from 'components/FlagSelect' +import FeatureSelect from 'components/FeatureSelect' import InfoMessage from 'components/InfoMessage' import EnvironmentSelect from 'components/EnvironmentSelect' import SegmentOverrideLimit from 'components/SegmentOverrideLimit' @@ -495,7 +495,7 @@ class SegmentOverridesInnerAdd extends Component { return (
{!readOnly && ( - { - if (this.props.environmentFlag) { - AppActions.getFeatureUsage( - this.props.projectId, - this.props.environmentFlag.environment, - this.props.projectFlag.id, - this.state.period, - ) - } - } save = (func, isSaving) => { const { environmentFlag, @@ -425,53 +408,6 @@ const CreateFlag = class extends Component { } return { featureError, featureWarning } } - drawChart = (data) => { - return data?.length ? ( - - - - - - - - - - ) : ( -
- There has been no activity for this flag within the past month. Find out - about Flag Analytics{' '} - - . -
- ) - } addItem = () => { const { environmentFlag, environmentId, identity, projectFlag } = this.props @@ -1816,36 +1752,6 @@ const CreateFlag = class extends Component { /> )} - {!Project.disableAnalytics && ( - - - {!!usageData && ( -
- Flag events for last 30 days -
- )} - {!usageData && ( -
- -
- )} - - {this.drawChart(usageData)} -
- - The Flag Analytics data will be visible in - the Dashboard between 30 minutes and 1 - hour after it has been collected.{' '} - - View docs - - -
- )} {hasIntegrationWithGithub && projectFlag?.id && ( = (props) => { environmentId={environment} projectId={projectId} searchPanel={ - - {({ project }: { project: Project }) => ( - - {project && - project.environments && - project.environments.map((env, i) => ( - { - setEnvironment( - `${environment}` === `${env.id}` - ? undefined - : env.id, - ) - }} - className='mr-2 mb-2' - /> - ))} - - )} - + } /> diff --git a/frontend/web/components/pages/FeatureAnalyticsPage.tsx b/frontend/web/components/pages/FeatureAnalyticsPage.tsx new file mode 100644 index 000000000000..ae3c89898f00 --- /dev/null +++ b/frontend/web/components/pages/FeatureAnalyticsPage.tsx @@ -0,0 +1,71 @@ +import React, { FC, useCallback } from 'react' +import { RouterChildContext } from 'react-router' +import PageTitle from 'components/PageTitle' +import FeatureSelect from 'components/FeatureSelect' +import Utils from 'common/utils/utils' +import ConfigProvider from 'common/providers/ConfigProvider' +import FeatureAnalytics from 'components/FeatureAnalytics' +import InputGroup from 'components/base/forms/InputGroup' +import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' + +type FeatureAnalyticsPageType = { + router: RouterChildContext['router'] + match: { + params: { + environmentId: string + projectId: string + } + } +} + +const FeatureAnalyticsPage: FC = ({ + match, + router, +}) => { + const { data: environments } = useGetEnvironmentsQuery({ + projectId: match.params.projectId, + }) + const params = Utils.fromParam() + const selectedFlag = params.feature + const setSelectedFlag = useCallback( + (flag) => { + router.history.replace(`${document.location.pathname}?feature=${flag}`) + }, + [router], + ) + return ( +
+ +
View how often your applications are evaluating features.
+
+
+
+ + } + /> +
+
+ {!!selectedFlag && environments && ( + `${v.id}`) + } + /> + )} +
+ ) +} + +export default ConfigProvider(FeatureAnalyticsPage) diff --git a/frontend/web/routes.js b/frontend/web/routes.js index c98c636d3186..6bff648b4cea 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -40,6 +40,7 @@ import SDKKeysPage from './components/SDKKeysPage' import { ParameterizedRoute } from './components/base/higher-order/ParameterizedRoute' import FeatureHistoryDetailPage from './components/pages/FeatureHistoryDetailPage' import OrganisationIntegrationsPage from './components/pages/OrganisationIntegrationsPage' +import FeatureAnalyticsPage from 'components/pages/FeatureAnalyticsPage'; export const routes = { 'account': '/account', @@ -68,6 +69,7 @@ export const routes = { 'login': '/login', 'maintenance': '/maintenance', 'not-found': '/404', + 'feature-analytics': '/project/:projectId/feature-analytics', 'oauth': '/oauth/:type', 'organisation-integrations': '/organisation/:organisationId/integrations', 'organisation-permissions': '/organisation/:organisationId/permissions', @@ -158,6 +160,11 @@ export default ( exact component={OrganisationIntegrationsPage} /> +