diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 10aac6cf8..50bfc9566 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -63,8 +63,11 @@ const DASHBOARD_SELECT_PATH = routes.dashboardSelected(); const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate(); const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails(); -// const WIDGET_PATAH = routes.dashboardMetric(); const SESSIONS_PATH = routes.sessions(); +const FFLAGS_PATH = routes.fflags(); +const FFLAG_PATH = routes.fflag(); +const FFLAG_CREATE_PATH = routes.newFFlag(); +const NOTES_PATH = routes.notes(); const ASSIST_PATH = routes.assist(); const RECORDINGS_PATH = routes.recordings(); // const ERRORS_PATH = routes.errors(); @@ -232,6 +235,10 @@ class Router extends React.Component { + + + + } /> diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index c9a041cc9..41d6fd89f 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -28,6 +28,7 @@ const siteIdRequiredPaths = [ '/cards', '/unprocessed', '/notes', + '/feature-flags', // '/custom_metrics/sessions', ]; diff --git a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx index 00fc42349..9290ff799 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx @@ -47,21 +47,6 @@ function Header({ history, siteId }: { history: any; siteId: string }) { }) } /> - {/*
diff --git a/frontend/app/components/FFlags/FFlagItem/FFlagItem.tsx b/frontend/app/components/FFlags/FFlagItem/FFlagItem.tsx new file mode 100644 index 000000000..09886902a --- /dev/null +++ b/frontend/app/components/FFlags/FFlagItem/FFlagItem.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import FeatureFlag from 'App/mstore/types/FeatureFlag' +import { Icon, Toggler, Link } from 'UI' +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import { resentOrDate } from 'App/date'; +import { toast } from 'react-toastify'; + +function FFlagItem({ flag }: { flag: FeatureFlag }) { + const { featureFlagsStore, userStore } = useStore(); + + const toggleActivity = () => { + flag.setIsEnabled(!flag.isActive); + featureFlagsStore.updateFlag(flag, true).then(() => { + toast.success('Feature flag updated.'); + }) + } + + const flagIcon = flag.isSingleOption ? 'fflag-single' : 'fflag-multi' as const + const flagOwner = flag.updatedBy || flag.createdBy + const user = userStore.list.length > 0 ? userStore.list.find(u => parseInt(u.userId) === flagOwner!)?.name : flagOwner; + return ( +
+
+ +
+ + {flag.flagKey} +
+ +
{flag.isSingleOption ? 'Single Option' : 'Multivariant'}
+
{resentOrDate(flag.updatedAt || flag.createdAt)}
+
+ + {user} +
+
+ +
+
+ {flag.description ?
{flag.description}
: null} +
+ ); +} + +export default observer(FFlagItem); diff --git a/frontend/app/components/FFlags/FFlagItem/index.ts b/frontend/app/components/FFlags/FFlagItem/index.ts new file mode 100644 index 000000000..c8d990b98 --- /dev/null +++ b/frontend/app/components/FFlags/FFlagItem/index.ts @@ -0,0 +1 @@ +export { default } from './FFlagItem' \ No newline at end of file diff --git a/frontend/app/components/FFlags/FFlagsList.tsx b/frontend/app/components/FFlags/FFlagsList.tsx new file mode 100644 index 000000000..eb1812648 --- /dev/null +++ b/frontend/app/components/FFlags/FFlagsList.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import FFlagsListHeader from 'Components/FFlags/FFlagsListHeader'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { Loader, NoContent } from 'UI'; +import FFlagItem from './FFlagItem'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import Select from 'Shared/Select'; + +function FFlagsList({ siteId }: { siteId: string }) { + const { featureFlagsStore, userStore } = useStore(); + + React.useEffect(() => { + void featureFlagsStore.fetchFlags(); + void userStore.fetchUsers(); + }, []); + + return ( +
+ + +
+ + +
+ You haven't created any feature flags yet. +
+
+ } + subtext={ +
+ Use feature flags to deploy and rollback new functionality with ease. +
+ } + > +
+
+
+ Status: + { + featureFlagsStore.setSort({ query: '', order: value.value }) + void featureFlagsStore.fetchFlags(); + }} + /> +
+
+
+
Key
+
Type
+
Last modified
+
Last modified by
+
Status
+
+ + {featureFlagsStore.flags.map((flag) => ( + + + + ))} +
+ +
+ +
+ ); +} + +export default observer(FFlagsList); diff --git a/frontend/app/components/FFlags/FFlagsListHeader.tsx b/frontend/app/components/FFlags/FFlagsListHeader.tsx new file mode 100644 index 000000000..49c28c888 --- /dev/null +++ b/frontend/app/components/FFlags/FFlagsListHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { Button, PageTitle } from 'UI' +import FFlagsSearch from "Components/FFlags/FFlagsSearch"; +import { useHistory } from "react-router"; +import { newFFlag, withSiteId } from 'App/routes'; +import ReloadButton from "Shared/ReloadButton"; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; + +function FFlagsListHeader({ siteId }: { siteId: string }) { + const history = useHistory(); + const { featureFlagsStore } = useStore(); + + const onReload = () => { + void featureFlagsStore.fetchFlags(); + } + return ( +
+
+ + +
+
+ +
+
+ +
+
+
+ ) +} + +export default observer(FFlagsListHeader); \ No newline at end of file diff --git a/frontend/app/components/FFlags/FFlagsSearch.tsx b/frontend/app/components/FFlags/FFlagsSearch.tsx new file mode 100644 index 000000000..4ec13d3f5 --- /dev/null +++ b/frontend/app/components/FFlags/FFlagsSearch.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from 'react'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { Icon } from 'UI'; +import { debounce } from 'App/utils'; + +let debounceUpdate: any = () => {}; + +function FFlagsSearch() { + const { featureFlagsStore } = useStore(); + const [query, setQuery] = useState(featureFlagsStore.flagsSearch); + + useEffect(() => { + debounceUpdate = debounce( + (value: string) => { + featureFlagsStore.setSort({ order: featureFlagsStore.sort.order, query: value }) + void featureFlagsStore.fetchFlags() + }, + 250 + ); + }, []); + + const write = ({ target: { value } }: React.ChangeEvent) => { + setQuery(value); + debounceUpdate(value); + }; + + return ( +
+ + +
+ ); +} + +export default observer(FFlagsSearch); diff --git a/frontend/app/components/FFlags/NewFFlag/Conditions.tsx b/frontend/app/components/FFlags/NewFFlag/Conditions.tsx new file mode 100644 index 000000000..e8f5cc6c3 --- /dev/null +++ b/frontend/app/components/FFlags/NewFFlag/Conditions.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Icon, Input, Button } from 'UI'; +import cn from 'classnames'; +import FilterList from 'Shared/Filters/FilterList'; +import { nonFlagFilters } from 'Types/filter/newFilter'; +import { observer } from 'mobx-react-lite'; +import { Conditions } from "App/mstore/types/FeatureFlag"; + +interface Props { + set: number; + conditions: Conditions; + removeCondition: (ind: number) => void; + index: number +} + +function RolloutCondition({ set, conditions, removeCondition, index }: Props) { + const [forceRender, forceRerender] = React.useState(false); + const onAddFilter = () => { + conditions.filter.addFilter({}); + forceRerender(!forceRender); + }; + const onUpdateFilter = (filterIndex: number, filter: any) => { + conditions.filter.updateFilter(filterIndex, filter); + forceRerender(!forceRender); + }; + + const onChangeEventsOrder = (_: any, { name, value }: any) => { + conditions.filter.updateKey(name, value); + forceRerender(!forceRender); + }; + + const onRemoveFilter = (filterIndex: number) => { + conditions.filter.removeFilter(filterIndex); + forceRerender(!forceRender); + }; + + const onPercentChange = (e: React.ChangeEvent) => { + const value = e.target.value || '0'; + if (value.length > 3) return; + if (parseInt(value, 10) > 100) return conditions.setRollout(100); + conditions.setRollout(parseInt(value, 10)); + }; + + return ( +
+
+
Condition
+
Set {set}
+
removeCondition(index)} + > + +
+
+
+
0 ? 'p-2 border-b mb-2' : ''}> + +
+ +
+
+ Rollout to + %
} + /> + of sessions +
+ + ); +} + +export default observer(RolloutCondition); diff --git a/frontend/app/components/FFlags/NewFFlag/Description.tsx b/frontend/app/components/FFlags/NewFFlag/Description.tsx new file mode 100644 index 000000000..cfcbab77d --- /dev/null +++ b/frontend/app/components/FFlags/NewFFlag/Description.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { Button } from 'UI'; +import cn from 'classnames'; +import FeatureFlag from 'MOBX/types/FeatureFlag'; + +function Description({ + isDescrEditing, + current, + setEditing, + showDescription, +}: { + showDescription: boolean; + isDescrEditing: boolean; + current: FeatureFlag; + setEditing: ({ isDescrEditing }: { isDescrEditing: boolean }) => void; +}) { + return ( + <> + + {isDescrEditing ? ( +