From 523feff2e0dfbffd25fdee75b75e97df8710f6de Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 Apr 2026 12:42:16 +0100 Subject: [PATCH] migrate get features + remove datadog widget --- .../common/dispatcher/action-constants.js | 2 - frontend/common/dispatcher/app-actions.js | 42 -- frontend/common/stores/feature-list-store.ts | 32 +- frontend/common/stores/project-store.js | 100 +--- frontend/common/types/requests.ts | 1 + frontend/package-lock.json | 27 - frontend/package.json | 2 - frontend/web/components/App.js | 8 - .../web/components/OrgEnvironmentSelect.tsx | 182 ------ frontend/web/components/datadog-client.ts | 21 - .../import-export/FeatureExport.tsx | 156 ++--- .../import-export/FeatureImport.tsx | 111 ++-- frontend/web/components/pages/UserPage.tsx | 111 ++-- frontend/web/components/pages/WidgetPage.tsx | 548 ------------------ .../web/components/useSetupCustomWidget.ts | 47 -- frontend/web/routes.js | 3 - 16 files changed, 220 insertions(+), 1173 deletions(-) delete mode 100644 frontend/web/components/OrgEnvironmentSelect.tsx delete mode 100644 frontend/web/components/datadog-client.ts delete mode 100644 frontend/web/components/pages/WidgetPage.tsx delete mode 100644 frontend/web/components/useSetupCustomWidget.ts diff --git a/frontend/common/dispatcher/action-constants.js b/frontend/common/dispatcher/action-constants.js index 2d7515bc6c52..5aeb81a34d6e 100644 --- a/frontend/common/dispatcher/action-constants.js +++ b/frontend/common/dispatcher/action-constants.js @@ -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_FLAGS': 'GET_FLAGS', 'GET_IDENTITY': 'GET_IDENTITY', 'GET_ORGANISATION': 'GET_ORGANISATION', 'GET_PROJECT': 'GET_PROJECT', @@ -37,7 +36,6 @@ const Actions = Object.assign({}, require('./base/_action-constants'), { 'REMOVE_FLAG': 'REMOVE_FLAG', 'REMOVE_USER_FLAG': 'REMOVE_USER_FLAG', 'RESEND_INVITE': 'RESEND_INVITE', - 'SEARCH_FLAGS': 'SEARCH_FLAGS', 'SELECT_ENVIRONMENT': 'SELECT_ENVIRONMENT', 'SELECT_ORGANISATION': 'SELECT_ORGANISATION', 'TOGGLE_USER_FLAG': 'TOGGLE_USER_FLAG', diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index ed10278ba5e6..f757bac855f4 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -199,28 +199,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { projectId, }) }, - getFeatures( - projectId, - environmentId, - force, - search, - sort, - page, - filter, - pageSize, - ) { - Dispatcher.handleViewAction({ - actionType: Actions.GET_FLAGS, - environmentId, - filter, - force, - page, - pageSize, - projectId, - search, - sort, - }) - }, getIdentity(envId, id) { Dispatcher.handleViewAction({ actionType: Actions.GET_IDENTITY, @@ -306,26 +284,6 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { name, }) }, - searchFeatures( - projectId, - environmentId, - force, - search, - sort, - filter, - pageSize, - ) { - Dispatcher.handleViewAction({ - actionType: Actions.SEARCH_FLAGS, - environmentId, - filter, - force, - pageSize, - projectId, - search, - sort, - }) - }, selectOrganisation(id) { Dispatcher.handleViewAction({ actionType: Actions.SELECT_ORGANISATION, diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index ad646bd0e9d1..93d1a82c9fee 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -1,5 +1,4 @@ import Constants from 'common/constants' -import { getIsWidget } from 'components/pages/WidgetPage' import ProjectStore from './project-store' import { createAndSetFeatureVersion, @@ -935,9 +934,7 @@ const controller = { store.loaded() }) .catch((e) => { - if (!getIsWidget()) { - document.location.href = '/404?entity=environment' - } + document.location.href = '/404?entity=environment' API.ajaxHandler(store, e) }) }, @@ -1020,33 +1017,6 @@ store.dispatcherIndex = Dispatcher.register(store, (payload) => { const action = payload.action // this is our action from handleViewAction const projectId = parseInt(action.projectId) switch (action.actionType) { - case Actions.SEARCH_FLAGS: { - if (action.sort) { - store.sort = action.sort - } - controller.searchFeatures( - action.search, - action.environmentId, - projectId, - action.filter, - action.pageSize, - ) - break - } - case Actions.GET_FLAGS: - store.search = encodeURIComponent(action.search || '') - if (action.sort) { - store.sort = action.sort - } - controller.getFeatures( - projectId, - action.environmentId, - action.force, - action.page, - action.filter, - action.pageSize, - ) - break case Actions.REFRESH_FEATURES: if ( projectId === store.projectId && diff --git a/frontend/common/stores/project-store.js b/frontend/common/stores/project-store.js index 8e1ef28c380e..f6deee3c5c61 100644 --- a/frontend/common/stores/project-store.js +++ b/frontend/common/stores/project-store.js @@ -1,4 +1,3 @@ -import { getIsWidget } from 'components/pages/WidgetPage' import OrganisationStore from './organisation-store' import Constants from 'common/constants' @@ -130,76 +129,41 @@ const controller = { }, getProject: (id, cb, force) => { if (!id) { - if (!getIsWidget()) { - !force && AsyncStorage.removeItem('lastEnv') - document.location.href = '/404' - } - } else if (force) { - store.loading() + !force && AsyncStorage.removeItem('lastEnv') + document.location.href = '/404' + return + } - return Promise.all([ - data.get(`${Project.api}projects/${id}/`), - data.get(`${Project.api}environments/?project=${id}`).catch(() => []), - ]) - .then(([project, environments]) => { - project.max_segments_allowed = project.max_segments_allowed - project.max_features_allowed = project.max_features_allowed - project.max_segment_overrides_allowed = - project.max_segment_overrides_allowed - project.total_features = project.total_features || 0 - project.total_segments = project.total_segments || 0 - store.model = Object.assign(project, { - environments: _.sortBy(environments.results, 'name'), - }) - if (project.organisation !== OrganisationStore.id) { - AppActions.selectOrganisation(project.organisation) - AppActions.getOrganisation(project.organisation) - } - store.id = id - store.loaded() - if (cb) { - cb() - } - }) - .catch(() => { - if (!getIsWidget()) { - document.location.href = '/404?entity=project' - } - }) - } else if (!store.model || !store.model.environments || store.id !== id) { - store.loading() + if (!force && store.model && store.model.environments && store.id === id) { + return + } - Promise.all([ - data.get(`${Project.api}projects/${id}/`), - data.get(`${Project.api}environments/?project=${id}`).catch(() => []), - ]) - .then(([project, environments]) => { - project.max_segments_allowed = project.max_segments_allowed - project.max_features_allowed = project.max_features_allowed - project.max_segment_overrides_allowed = - project.max_segment_overrides_allowed - project.total_features = project.total_features || 0 - project.total_segments = project.total_segments || 0 - store.model = Object.assign(project, { - environments: _.sortBy(environments.results, 'name'), - }) - if (project.organisation !== OrganisationStore.id) { - AppActions.selectOrganisation(project.organisation) - AppActions.getOrganisation(project.organisation) - } - store.id = id - store.loaded() - if (cb) { - cb() - } - }) - .catch(() => { - if (!getIsWidget()) { - AsyncStorage.removeItem('lastEnv') - document.location.href = '/404?entity=project' - } + store.loading() + + return Promise.all([ + data.get(`${Project.api}projects/${id}/`), + data.get(`${Project.api}environments/?project=${id}`).catch(() => []), + ]) + .then(([project, environments]) => { + project.total_features = project.total_features || 0 + project.total_segments = project.total_segments || 0 + store.model = Object.assign(project, { + environments: _.sortBy(environments.results, 'name'), }) - } + if (project.organisation !== OrganisationStore.id) { + AppActions.selectOrganisation(project.organisation) + AppActions.getOrganisation(project.organisation) + } + store.id = id + store.loaded() + if (cb) { + cb() + } + }) + .catch(() => { + AsyncStorage.removeItem('lastEnv') + document.location.href = '/404?entity=project' + }) }, migrateProject: (id) => { store.loading() diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index fc48e4e17841..6ef410f1ee75 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -868,6 +868,7 @@ export type Req = { tag_strategy?: TagStrategy sort_field?: string sort_direction?: 'ASC' | 'DESC' + identity?: string } updateFeatureState: { environmentId: string diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4782efe7eed6..4fbcbd37717c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,8 +20,6 @@ "@babel/preset-react": "^7.12.1", "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.12.1", - "@datadog/ui-extensions-react": "0.32.0", - "@datadog/ui-extensions-sdk": "0.32.0", "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/helpers": "^0.3.2", "@dnd-kit/react": "^0.3.2", @@ -2387,31 +2385,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@datadog/framepost": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@datadog/framepost/-/framepost-0.3.1.tgz", - "integrity": "sha512-MxK/stjASi0Djy5OS/teux7rfU6ffT4W3XrlFN5Xs1ZoOjZ3O+y9SOrOv9GKs+8TlL2l6UTiDWRfFUQZlrxJ1g==", - "license": "MIT" - }, - "node_modules/@datadog/ui-extensions-react": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@datadog/ui-extensions-react/-/ui-extensions-react-0.32.0.tgz", - "integrity": "sha512-9noao/6NaD9laRfL83RmpRcqiKAGXTCSbi8sVZgr9ztWezFCTnYUb6HD7o5bUXXHPDNnHbwsPFs6o4FTq/U2lw==", - "license": "Apache-2.0", - "peerDependencies": { - "@datadog/ui-extensions-sdk": "0.32.0", - "react": "^18.0.0" - } - }, - "node_modules/@datadog/ui-extensions-sdk": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@datadog/ui-extensions-sdk/-/ui-extensions-sdk-0.32.0.tgz", - "integrity": "sha512-3eJGis/WU48d0GVB5+3zEuJVyTARCltx7T6ANwtPZPXoc2XqHlkXrT1HUWT415EeOCNJuCFtjt5Gh7Tyle4moA==", - "license": "Apache-2.0", - "dependencies": { - "@datadog/framepost": "^0.3.0" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 27106b6514ee..bb1a6cd92a5a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,8 +50,6 @@ "@babel/preset-react": "^7.12.1", "@babel/preset-typescript": "^7.18.6", "@babel/register": "^7.12.1", - "@datadog/ui-extensions-react": "0.32.0", - "@datadog/ui-extensions-sdk": "0.32.0", "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/helpers": "^0.3.2", "@dnd-kit/react": "^0.3.2", diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 1db924ae0737..32ace0c54749 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -12,7 +12,6 @@ import AccountSettingsPage from './pages/AccountSettingsPage' import ProjectStore from 'common/stores/project-store' import { Provider } from 'react-redux' import { getStore } from 'common/store' -import { resolveAuthFlow } from '@datadog/ui-extensions-sdk' import ConfigProvider from 'common/providers/ConfigProvider' import AccountStore from 'common/stores/account-store' import OrganisationLimit from './OrganisationLimit' @@ -117,10 +116,6 @@ const App = class extends Component { } onLogin = () => { - resolveAuthFlow({ - isAuthenticated: true, - }) - let redirect = API.getRedirect() const invite = API.getInvite() if (invite) { @@ -209,9 +204,6 @@ const App = class extends Component { } onLogout = () => { - resolveAuthFlow({ - isAuthenticated: false, - }) if (document.location.href.includes('saml?')) { return } diff --git a/frontend/web/components/OrgEnvironmentSelect.tsx b/frontend/web/components/OrgEnvironmentSelect.tsx deleted file mode 100644 index bec979002fa0..000000000000 --- a/frontend/web/components/OrgEnvironmentSelect.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { FC, useMemo, useState } from 'react' -import { useGetOrganisationsQuery } from 'common/services/useOrganisation' -import { useGetProjectsQuery } from 'common/services/useProject' -import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' -import { Environment } from 'common/types/responses' -import Format from 'common/utils/format' -import { sortBy } from 'lodash' -import PanelSearch from './PanelSearch' -import Input from './base/forms/Input' -import Button from './base/forms/Button' -import Utils from 'common/utils/utils' - -type OrgProjectSelectType = { - organisationId?: string | null - onOrganisationChange: (id: string | null) => void - projectId?: string | null - onProjectChange: (id: string | null) => void - environmentId?: string | null - useApiKey?: boolean - onEnvironmentChange: (id: string | null) => void -} - -const OrgEnvironmentSelect: FC = ({ - environmentId, - onEnvironmentChange, - onOrganisationChange, - onProjectChange, - organisationId, - projectId, - useApiKey, -}) => { - const [search, setSearch] = useState() - - const { data: organisations, isLoading: organisationsLoading } = - useGetOrganisationsQuery({}) - const { data: projects, isLoading: projectsLoading } = useGetProjectsQuery( - { organisationId: organisationId as string }, - { skip: !organisationId }, - ) - const { data: environments, isLoading: environmentsLoading } = - useGetEnvironmentsQuery( - { projectId: projectId as string }, - { skip: !projectId }, - ) - const organisation = useMemo( - () => organisations?.results?.find((v) => `${v.id}` === organisationId), - [organisations, organisationId], - ) - const project = useMemo( - () => projects?.find((v) => `${v.id}` === projectId), - [projects, projectId], - ) - const environment = useMemo( - () => environments?.results?.find((v) => `${v.api_key}` === environmentId), - [environments, environmentId], - ) - let onClick = onEnvironmentChange - let items: any = environments?.results - let level = 'ENVIRONMENT' - - if (!organisation) { - onClick = onOrganisationChange - items = organisations?.results - level = 'ORGANISATION' - } else if (!project) { - onClick = onProjectChange - items = projects - level = 'PROJECT' - } - return ( - <> - - {organisation && ( - onOrganisationChange(null)}> - Organisations - - )} - {organisation && ( - <> - - onProjectChange(null)}> - {organisation.name} - - - )} - {project && ( - <> - - onProjectChange(null)}> - {project.name} - - - )} - {environment && ( - <> - - {environment.name} - - )} - - {environmentId && projectId ? ( -
-

Copy the following values to your widget configuration.

- - - Project ID - - - - - - - Environment ID - - - - -
- ) : ( - { - return v.name - })} - filterRow={(row: Environment, search: string) => - row.name.toLowerCase().includes(search.toLowerCase()) - } - onChange={setSearch} - search={search} - renderRow={({ api_key, id, name }) => ( - { - onClick(`${useApiKey ? api_key || id : id}`) - }} - > - {name} - - )} - /> - )} - - ) -} - -export default OrgEnvironmentSelect diff --git a/frontend/web/components/datadog-client.ts b/frontend/web/components/datadog-client.ts deleted file mode 100644 index 89e5da5fcdeb..000000000000 --- a/frontend/web/components/datadog-client.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DDClient, init } from '@datadog/ui-extensions-sdk' -import API from 'project/api' - -const client: () => DDClient = () => - init({ - authProvider: { - authStateCallback: async () => { - const user = API.getCookie('t') - return { - isAuthenticated: !!user, - } - }, - resolution: 'message', - - /** - * This where we want Datadog to direct users to authenticate. - */ - url: '/', - }, - }) -export default client diff --git a/frontend/web/components/import-export/FeatureExport.tsx b/frontend/web/components/import-export/FeatureExport.tsx index 3ce59a4ee42a..a92c17244f72 100644 --- a/frontend/web/components/import-export/FeatureExport.tsx +++ b/frontend/web/components/import-export/FeatureExport.tsx @@ -5,16 +5,8 @@ import { IonIcon } from '@ionic/react' import { informationCircle } from 'ionicons/icons' import TagFilter from 'components/tags/TagFilter' import PanelSearch from 'components/PanelSearch' -import FeatureListStore from 'common/stores/feature-list-store' -import FeatureListProvider from 'common/providers/FeatureListProvider' -import AppActions from 'common/dispatcher/app-actions' import FeatureRow from 'components/feature-summary/FeatureRow' -import { - FeatureListProviderData, - FeatureState, - ProjectFlag, - TagStrategy, -} from 'common/types/responses' +import { ProjectFlag, TagStrategy } from 'common/types/responses' import ProjectStore from 'common/stores/project-store' import Utils from 'common/utils/utils' import Button from 'components/base/forms/Button' @@ -24,6 +16,8 @@ import { } from 'common/services/useFeatureExport' import InfoMessage from 'components/InfoMessage' import FeatureExportItem from './FeatureExportItem' +import { useGetFeatureListQuery } from 'common/services/useProjectFlag' +import { useProjectEnvironments } from 'common/hooks/useProjectEnvironments' type FeatureExportType = { projectId: string @@ -35,18 +29,37 @@ const FeatureExport: FC = ({ projectId }) => { ) const [tags, setTags] = useState<(number | string)[]>([]) const [search, setSearch] = useState() - const [page, setPage] = useState(0) + const [page, setPage] = useState(1) const [tagStrategy, setTagStrategy] = useState('UNION') - useEffect(() => { - if (environment) { - AppActions.getFeatures(projectId, environment, true, null, null, page, { - search, - tag_strategy: tagStrategy, - tags: tags?.length ? tags : undefined, - }) - } - }, [environment, tagStrategy, tags, search, projectId, page]) + const { getEnvironmentIdFromKey, isLoading: isLoadingEnvs } = + useProjectEnvironments(parseInt(projectId)) + + const environmentId = environment + ? getEnvironmentIdFromKey(environment) + : undefined + + const { data: featureListData, isFetching: isLoadingFeatures } = + useGetFeatureListQuery( + environmentId + ? { + environmentId: String(environmentId), + page, + projectId: parseInt(projectId), + search: search || undefined, + sort_direction: 'ASC', + sort_field: 'name', + tag_strategy: tagStrategy, + tags: tags?.length ? tags.join(',') : undefined, + } + : ({} as any), + { skip: !environmentId }, + ) + + const projectFlags = featureListData?.results + const environmentFlags = featureListData?.environmentStates + const paging = featureListData?.pagination + const [createFeatureExport, { isLoading: isCreating, isSuccess }] = useCreateFeatureExportMutation({}) @@ -125,64 +138,57 @@ const FeatureExport: FC = ({ projectId }) => { value={tags} onChange={setTags} /> - - {({ - environmentFlags, - projectFlags, - }: { - environmentFlags?: FeatureListProviderData['environmentFlags'] - projectFlags: FeatureListProviderData['projectFlags'] - }) => { - const isLoading = !FeatureListStore.hasLoaded - - if (isLoading) { - return ( -
- -
- ) - } + {(() => { + if (isLoadingEnvs || (isLoadingFeatures && !projectFlags)) { return ( - <> -
- Features{' '} -
- { - setSearch(Utils.safeParseEventValue(e)) - }} - isLoading={FeatureListStore.isLoading} - paging={FeatureListStore.paging} - nextPage={() => setPage(page + 1)} - renderRow={(projectFlag, i) => ( - - )} - prevPage={() => setPage(page - 1)} - goToPage={setPage} - /> -
- -
- +
+ +
) - }} -
+ } + return ( + <> +
+ Features{' '} +
+ { + setSearch(Utils.safeParseEventValue(e)) + }} + isLoading={isLoadingFeatures} + paging={paging} + nextPage={() => setPage(page + 1)} + renderRow={(projectFlag: ProjectFlag, i: number) => ( + + )} + prevPage={() => setPage(page - 1)} + goToPage={setPage} + /> +
+ +
+ + ) + })()} {isCreating || !exports ? (
diff --git a/frontend/web/components/import-export/FeatureImport.tsx b/frontend/web/components/import-export/FeatureImport.tsx index bb50bc0bf530..d8a482ff15bf 100644 --- a/frontend/web/components/import-export/FeatureImport.tsx +++ b/frontend/web/components/import-export/FeatureImport.tsx @@ -1,9 +1,8 @@ -import React, { FC, useEffect, useMemo, useState } from 'react' +import React, { FC, useMemo, useState } from 'react' import EnvironmentSelect from 'components/EnvironmentSelect' import Tooltip from 'components/Tooltip' import { IonIcon } from '@ionic/react' import { informationCircle } from 'ionicons/icons' -import AppActions from 'common/dispatcher/app-actions' import ProjectStore from 'common/stores/project-store' import Radio from 'components/base/forms/Radio' import { ImportStrategy } from 'common/types/responses' @@ -13,7 +12,6 @@ import PanelSearch from 'components/PanelSearch' import { FeatureImportItem, FeatureState, - ProjectFlag, TagStrategy, } from 'common/types/responses' import FeatureRow from 'components/feature-summary/FeatureRow' @@ -22,7 +20,6 @@ import { useCreateFlagsmithProjectImportMutation } from 'common/services/useFlag import ErrorMessage from 'components/ErrorMessage' import InfoMessage from 'components/InfoMessage' import WarningMessage from 'components/WarningMessage' -import FeatureListStore from 'common/stores/feature-list-store' import SuccessMessage from 'components/messages/SuccessMessage' import TableSearchFilter from 'components/tables/TableSearchFilter' import Utils from 'common/utils/utils' @@ -31,6 +28,8 @@ import TableFilterOptions from 'components/tables/TableFilterOptions' import { getViewMode, setViewMode } from 'common/useViewMode' import TableSortFilter, { SortValue } from 'components/tables/TableSortFilter' import { useGetFeatureImportsQuery } from 'common/services/useFeatureImport' +import { useGetFeatureListQuery } from 'common/services/useProjectFlag' +import { useProjectEnvironments } from 'common/hooks/useProjectEnvironments' type FeatureExportType = { projectId: string @@ -48,26 +47,22 @@ const FeatureExport: FC = ({ projectId }) => { }) const [tags, setTags] = useState<(number | string)[]>([]) const [search, setSearch] = useState('') - const [page, setPage] = useState(0) + const [page, setPage] = useState(1) const env = ProjectStore.getEnvironment(environment) const [file, setFile] = useState(null) const [fileData, setFileData] = useState(null) - const [currentProjectflags, setCurrentProjectflags] = - useState() - const [currentFeatureStates, setCurrentFeatureStates] = - useState>() - useEffect(() => { - const callback = () => { - setCurrentProjectflags(FeatureListStore.getProjectFlags()) - setCurrentFeatureStates(FeatureListStore.getEnvironmentFlags()) - } - FeatureListStore.on('change', callback) + const { getEnvironmentIdFromKey, isLoading: _isLoadingEnvs } = + useProjectEnvironments(parseInt(projectId)) + + const previewEnvId = previewEnvironment + ? getEnvironmentIdFromKey(previewEnvironment) + : undefined + + const baseEnvId = environment + ? getEnvironmentIdFromKey(environment) + : undefined - return () => { - FeatureListStore.off('change', callback) - } - }, []) const [showArchived, setShowArchived] = useState(false) const [tagStrategy, setTagStrategy] = useState('UNION') @@ -77,31 +72,42 @@ const FeatureExport: FC = ({ projectId }) => { sortOrder: SortOrder.ASC, }) - useEffect(() => { - if (previewEnvironment) { - AppActions.getFeatures( - projectId, - previewEnvironment, - true, - search, - null, - page, - { + // Fetch features for the preview environment (with filters/search) + const { isFetching: isLoadingPreview } = useGetFeatureListQuery( + previewEnvId + ? { + environmentId: String(previewEnvId), is_archived: showArchived, + page, + projectId: parseInt(projectId), + search: search || undefined, + sort_direction: sort.sortOrder, + sort_field: sort.sortBy, tag_strategy: tagStrategy, - tags: tags?.length ? tags : undefined, - }, - ) - } - }, [ - previewEnvironment, - tagStrategy, - showArchived, - tags, - search, - projectId, - page, - ]) + tags: tags?.length ? tags.join(',') : undefined, + } + : ({} as any), + { skip: !previewEnvId }, + ) + + // Fetch base environment features for comparison with import data + const { data: baseFeatureData } = useGetFeatureListQuery( + baseEnvId + ? { + environmentId: String(baseEnvId), + page: 1, + projectId: parseInt(projectId), + search: search || undefined, + sort_direction: 'ASC', + sort_field: 'name', + tags: tags?.length ? tags.join(',') : undefined, + } + : ({} as any), + { skip: !baseEnvId }, + ) + + const currentProjectflags = baseFeatureData?.results + const currentFeatureStates = baseFeatureData?.environmentStates const [createImport, { error, isLoading }] = useCreateFlagsmithProjectImportMutation({}) @@ -129,12 +135,6 @@ const FeatureExport: FC = ({ projectId }) => { const [strategy, setStrategy] = useState('SKIP') - useEffect(() => { - AppActions.getFeatures(projectId, environment, true, null, null, page, { - search, - tags: tags?.length ? tags : undefined, - }) - }, [projectId, tags]) const { featureStates, projectFlags } = useMemo(() => { if (fileData) { const createdDate = new Date().toISOString() @@ -212,7 +212,15 @@ const FeatureExport: FC = ({ projectId }) => { return { featureStates, projectFlags } } return { featureStates: null, projectFlags: null } - }, [fileData, currentFeatureStates, currentProjectflags, strategy, env]) + }, [ + fileData, + currentFeatureStates, + currentProjectflags, + strategy, + env, + environment, + previewEnvironment, + ]) const filteredProjectFlags = useMemo(() => { return projectFlags?.filter((projectFlag) => { @@ -359,7 +367,7 @@ const FeatureExport: FC = ({ projectId }) => { /> = ({ projectId }) => { }} showArchived={showArchived} onChange={(tags) => { - FeatureListStore.isLoading = true if (tags.includes('') && tags.length > 1) { if (!tags.includes('')) { setTags(['']) @@ -399,7 +406,7 @@ const FeatureExport: FC = ({ projectId }) => { ]} /> { const match = useRouteMatch() const history = useHistory() - const params = Utils.fromParam() - const defaultState = parseFiltersFromUrlParams(params) const environmentId = match?.params?.environmentId const id = match?.params?.id const { projectId } = useRouteContext() - const [filter, setFilter] = useState(defaultState) + const { filters, goToPage, handleFilterChange, page } = + useFeatureFilters(history) const [actualFlags, setActualFlags] = useState>() const preselect = Utils.fromParam().flag @@ -80,13 +71,27 @@ const UserPage: FC = () => { { skip: !projectId }, ) - useEffect(() => { - const { search, sort } = filter - AppActions.searchFeatures(projectId, environmentId, true, search, sort, { - ...getServerFilter(filter), - identity: id, - }) - }, [filter, environmentId, projectId, id]) + const { getEnvironmentIdFromKey } = useProjectEnvironments(projectId!) + + const apiParams = environmentId + ? buildApiFilterParams( + filters, + page, + environmentId, + projectId!, + getEnvironmentIdFromKey, + ) + : null + + const { data: featureListData, isFetching: isLoadingFeatures } = + useGetFeatureListQuery( + apiParams ? { ...apiParams, identity: id } : ({} as any), + { skip: !apiParams }, + ) + + const paging = featureListData?.pagination + const projectFlags = featureListData?.results + const environmentFlags = featureListData?.environmentStates useEffect(() => { AppActions.getIdentity(environmentId, id) @@ -127,45 +132,24 @@ const UserPage: FC = () => { const preventAddTrait = !AccountStore.getOrganisation().persist_trait_data const showAliases = Utils.getIsEdge() - const fetchPage = React.useCallback( - (pageNumber: number) => { - const { search, sort } = filter - AppActions.getFeatures( - projectId, - environmentId, - true, - search, - sort, - pageNumber, - { ...getServerFilter(filter), identity: id }, - ) - }, - [environmentId, projectId, filter, id], - ) - return (
{({ - environmentFlags, identity, identityFlags, - isLoading, - projectFlags, + isLoading: isIdentityLoading, }: { - environmentFlags: FeatureState[] identity: { identity: Identity; identifier: string } identityFlags: IdentityFeatureState[] isLoading: boolean - projectFlags: ProjectFlag[] - traits: IdentityTrait[] }) => { const identityName = (identity && identity.identity.identifier) || id const isDataLoaded = !!actualFlags && !!identityFlags && !!projectFlags && !!projectId - return isLoading || !isDataLoaded ? ( + return isIdentityLoading || isLoadingFeatures || !isDataLoaded ? (
@@ -258,24 +242,23 @@ const UserPage: FC = () => { )} header={ { - FeatureListStore.isLoading = true - history.replace( - `${ - document.location.pathname - }?${Utils.toParam( - getURLParamsFromFilters(next), - )}`, - ) - setFilter(next) + handleFilterChange({ + ...next, + showArchived: next.is_archived, + }) }} /> } - isLoading={FeatureListStore.isLoading} + isLoading={isLoadingFeatures} items={projectFlags} renderRow={({ id: featureId, name }, i) => { const identityFlag = identityFlags[featureId] @@ -325,16 +308,14 @@ const UserPage: FC = () => { ) }} renderSearchWithNoResults - paging={FeatureListStore.paging} + paging={paging} nextPage={() => - fetchPage(FeatureListStore.paging.next) + goToPage((paging?.currentPage || 1) + 1) } prevPage={() => - fetchPage(FeatureListStore.paging.previous) - } - goToPage={(pageNumber: number) => - fetchPage(pageNumber) + goToPage((paging?.currentPage || 2) - 1) } + goToPage={goToPage} /> {!preventAddTrait && ( diff --git a/frontend/web/components/pages/WidgetPage.tsx b/frontend/web/components/pages/WidgetPage.tsx deleted file mode 100644 index 6c9ed2938883..000000000000 --- a/frontend/web/components/pages/WidgetPage.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import React, { - Component, - ReactNode, - useEffect, - useMemo, - useState, -} from 'react' -import TagFilter from 'components/tags/TagFilter' -import Tag from 'components/tags/Tag' -import FeatureRow from 'components/feature-summary/FeatureRow' -import FeatureListStore from 'common/stores/feature-list-store' -import ProjectStore from 'common/stores/project-store' -import API from 'project/api' -import { getStore } from 'common/store' -import Permission from 'common/providers/Permission' -import Constants from 'common/constants' -import Utils from 'common/utils/utils' -import { Provider } from 'react-redux' -import InfoMessage from 'components/InfoMessage' -import ProjectProvider from 'common/providers/ProjectProvider' -import AccountProvider from 'common/providers/AccountProvider' -import PanelSearch from 'components/PanelSearch' -// @ts-ignore -import { AsyncStorage } from 'polyfill-react-native' -import { SortOrder } from 'common/types/requests' -import { - Environment, - FeatureListProviderActions, - FeatureListProviderData, - Organisation, - PagedResponse, - Project, - ProjectFlag, - TagStrategy, -} from 'common/types/responses' -import { useCustomWidgetOptionString } from '@datadog/ui-extensions-react' -import ddClient from 'components/datadog-client' -import { resolveAuthFlow } from '@datadog/ui-extensions-sdk' -import AuditLog from 'components/AuditLog' -import OrgEnvironmentSelect from 'components/OrgEnvironmentSelect' -import AccountStore from 'common/stores/account-store' - -import FeatureListProvider from 'common/providers/FeatureListProvider' -import AppActions from 'common/dispatcher/app-actions' -import ES6Component from 'common/ES6Component' -let isWidget = false -export const getIsWidget = () => { - return isWidget -} - -type FeatureListType = { - projectId: string - environmentId: string - pageSize: number - hideTags: boolean -} - -const PermissionError = () => { - return ( - - Please check you have access to the project and environment within the - widget settings. - - ) -} - -type OrganisationWrapperType = { - projectId: string | undefined - children: ReactNode -} -const OrganisationWrapper = class extends Component { - constructor(props: any, context: any) { - super(props, context) - ES6Component(this) - if (this.props.projectId) { - AppActions.getProject(this.props.projectId) - } - } - - componentDidUpdate(prevProps: Readonly) { - if (this.props.projectId !== prevProps.projectId && this.props.projectId) { - AppActions.getProject(this.props.projectId) - } - } - render() { - if (!this.props.projectId) return <>{this.props.children} - return ( - - {() => ( - - {() => { - const project = ProjectStore.model as Project | null - if ( - project && - project?.organisation !== AccountStore.getOrganisation()?.id - ) { - // @ts-ignore - AccountStore.organisation = - AccountStore.getOrganisations()?.find( - (org: Organisation) => org.id === project.organisation, - ) - // @ts-ignore - if (!AccountStore.organisation) { - return null - } - } - // @ts-ignore - return AccountStore.model && ProjectStore.model ? ( - this.props.children - ) : ( -
- -
- ) - }} -
- )} -
- ) - } -} - -const FeatureList = class extends Component { - state = { - error: null as null | string, - search: null as null | string, - showArchived: false, - sort: { label: 'Name', sortBy: 'name', sortOrder: SortOrder.ASC }, - tag_strategy: 'INTERSECTION' as TagStrategy, - tags: [] as string[], - } - constructor(props: any, context: any) { - super(props, context) - ES6Component(this) - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - this.props.pageSize, - ) - } - - componentDidUpdate(prevProps: Readonly) { - if ( - this.props.projectId !== prevProps.projectId || - this.props.environmentId !== prevProps.environmentId || - this.props.pageSize !== prevProps.pageSize - ) { - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - this.props.pageSize, - ) - } - } - - componentDidMount = () => { - API.trackPage(Constants.pages.FEATURES) - } - - getFilter = () => ({ - is_archived: this.state.showArchived, - tag_strategy: this.state.tag_strategy, - tags: - !this.state.tags || !this.state.tags.length - ? undefined - : this.state.tags.join(','), - }) - - onSave = () => { - toast('Your feature has been updated') - } - - filter = () => { - AppActions.searchFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - this.getFilter(), - this.props.pageSize, - ) - } - - render() { - const { environmentId, projectId } = this.props - const environment = ProjectStore.getEnvironment( - environmentId, - ) as Environment | null - return ( - -
- - {( - { - environmentFlags, - error, - isLoading, - projectFlags, - }: FeatureListProviderData, - { removeFlag, toggleFlag }: FeatureListProviderActions, - ) => { - if (error) { - return - } - return ( -
- {projectFlags?.length === 0 && ( -
This project has no feature flags to display
- )} - {isLoading && (!projectFlags || !projectFlags.length) && ( -
- -
- )} - {(!isLoading || (projectFlags && !!projectFlags.length)) && ( -
- {(projectFlags && projectFlags.length) || - ((this.state.showArchived || - typeof this.state.search === 'string' || - !!this.state.tags.length) && - !isLoading) ? ( -
-
- { - this.setState( - { search: Utils.safeParseEventValue(e) }, - () => { - AppActions.searchFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - this.getFilter(), - this.props.pageSize, - ) - }, - ) - }} - nextPage={() => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).next || 1, - this.getFilter(), - this.props.pageSize, - ) - } - prevPage={() => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - ( - FeatureListStore.paging as PagedResponse - ).previous, - this.getFilter(), - this.props.pageSize, - ) - } - goToPage={(page: number) => - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - page, - this.getFilter(), - this.props.pageSize, - ) - } - onSortChange={(sort: string) => { - this.setState({ sort }, () => { - AppActions.getFeatures( - this.props.projectId, - this.props.environmentId, - true, - this.state.search, - this.state.sort, - 0, - this.getFilter(), - this.props.pageSize, - ) - }) - }} - sorting={[ - { - default: true, - label: 'Name', - order: SortOrder.ASC, - value: 'name', - }, - { - label: 'Created Date', - order: SortOrder.ASC, - value: 'created_date', - }, - ]} - items={projectFlags} - header={ - this.props.hideTags ? null : ( - - - this.setState( - { showArchived: false, tags: [] }, - this.filter, - ) - } - projectId={projectId} - value={this.state.tags} - tagStrategy={this.state.tag_strategy} - onChangeStrategy={(tag_strategy) => { - this.setState( - { tag_strategy }, - this.filter, - ) - }} - onChange={(tags) => { - FeatureListStore.isLoading = true - if ( - tags.includes('') && - tags.length > 1 - ) { - if (!this.state.tags.includes('')) { - this.setState( - { tags: [''] }, - this.filter, - ) - } else { - this.setState( - { - tags: tags.filter((v) => !!v), - }, - this.filter, - ) - } - } else { - this.setState({ tags }, this.filter) - } - AsyncStorage.setItem( - `${projectId}tags`, - JSON.stringify(tags), - ) - }} - > -
- { - FeatureListStore.isLoading = true - this.setState( - { - showArchived: - !this.state.showArchived, - }, - this.filter, - ) - }} - className='px-2 py-2 ml-2 mr-2' - tag={{ - color: '#0AADDF', - label: 'Archived', - }} - /> -
-
-
- ) - } - renderRow={( - projectFlag: ProjectFlag, - i: number, - ) => ( - - {({ permission }) => ( - - )} - - )} - filterRow={() => true} - /> -
-
- ) : null} -
- )} -
- ) - }} -
-
-
- ) - } -} - -export default function Widget() { - useEffect(() => { - document.body.classList.add('widget-mode') - }, []) - const client = useMemo(() => { - return ddClient() - }, []) - const projectId = useCustomWidgetOptionString(client, 'Project') - const environmentId = useCustomWidgetOptionString(client, 'Environment') - const pageSize = useCustomWidgetOptionString(client, 'PageSize') || '5' - // @ts-ignore context is marked as private but is accessible and needed - const id = client.context?.widget?.definition?.custom_widget_key - const isAudit = id === 'flagsmith_audit_widget' - const hideTags = useCustomWidgetOptionString(client, 'HideTags') === 'Yes' - const [error, setError] = useState(false) - const [_projectId, setProjectId] = useState(projectId || null) - const [_environmentId, setEnvironmentId] = useState( - environmentId || null, - ) - const [organisationId, setOrganisationId] = useState(null) - isWidget = true - - useEffect(() => { - setProjectId(environmentId || null) - }, [environmentId]) - - useEffect(() => { - setProjectId(projectId || null) - }, [projectId]) - - if (!API.getCookie('t')) { - resolveAuthFlow({ - isAuthenticated: false, - }) - return null - } - - if (error) { - return - } - if (projectId && environmentId && !error) { - if (isAudit) { - return ( - - -
- -
-
-
- ) - } - return ( - - - - ) - } - - return ( - -
-

Please select the environment you wish to use.

-
- - - -
-
-
- ) -} diff --git a/frontend/web/components/useSetupCustomWidget.ts b/frontend/web/components/useSetupCustomWidget.ts deleted file mode 100644 index 04e44f8bfbd2..000000000000 --- a/frontend/web/components/useSetupCustomWidget.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - DDClient, - EventType, - WidgetSettingsMenuClickData, -} from '@datadog/ui-extensions-sdk' -import * as React from 'react' -import API from 'project/api' - -/** - * This hook performs any app-wide for the custom widget. - * @param client The initialized {@link DDClient} - */ -function useSetupCustomWidget(client: DDClient): void { - /** - * We set up an event listener for the logout widget settings menu item. - * This event handler lets us perform the actual logging out of a user. - */ - React.useEffect(() => { - const unsubscribeLogout = client.events.on( - EventType.WIDGET_SETTINGS_MENU_CLICK, - async (data: WidgetSettingsMenuClickData): Promise => { - /** - * We only want to handle events from the `'logout'` settings menu item. - */ - if (data.menuItem.key !== 'logout') { - return - } - - /** - * Perform the actual logout, - * then make sure to update the auth state so it's reflected in Datadog. - */ - API.setCookie('t', '') - await client.auth.updateAuthState() - }, - ) - - /** - * We make sure to unsubscribe the event listener we set up. - */ - return () => { - unsubscribeLogout() - } - }, [client.auth, client.events]) -} - -export { useSetupCustomWidget } diff --git a/frontend/web/routes.js b/frontend/web/routes.js index fe8db11fca2a..531e0b7723bd 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -25,7 +25,6 @@ import ChangeRequestDetailPage from './components/pages/ChangeRequestDetailPage' import ScheduledChangesPage from './components/pages/ScheduledChangesPage' import AuditLogPage from './components/pages/AuditLogPage' import ComparePage from './components/pages/ComparePage' -import WidgetPage from './components/pages/WidgetPage' import BrokenPage from './components/pages/BrokenPage' import GitHubSetupPage from './components/pages/GitHubSetupPage' import AuditLogItemPage from './components/pages/AuditLogItemPage' @@ -123,7 +122,6 @@ export const routes = { 'user': '/project/:projectId/environment/:environmentId/users/:identity/:id', 'user-id': '/project/:projectId/environment/:environmentId/users/:identity', 'users': '/project/:projectId/environment/:environmentId/users', - 'widget': '/widget', } export default ( @@ -181,7 +179,6 @@ export default ( exact component={ChangeRequestDetailPage} /> -