From ae47db111e9b3198d67b9d35b7847a8d4d425016 Mon Sep 17 00:00:00 2001 From: Kyle Johnson Date: Tue, 10 Sep 2024 14:47:12 +0100 Subject: [PATCH] feat: Move versioned feature history into feature details modal (#4499) --- frontend/e2e/helpers.cafe.ts | 7 +- ...tureHistoryPage.tsx => FeatureHistory.tsx} | 119 +++++--------- frontend/web/components/FeatureRow.js | 4 +- .../web/components/base/forms/TabItem.tsx | 1 + frontend/web/components/base/forms/Tabs.js | 4 +- frontend/web/components/diff/DiffFeature.tsx | 9 +- frontend/web/components/diff/DiffSegments.tsx | 4 + frontend/web/components/modals/CreateFlag.js | 155 ++++++++++-------- .../pages/FeatureHistoryDetailPage.tsx | 21 +++ frontend/web/components/pages/HomeAside.tsx | 12 -- frontend/web/routes.js | 120 ++++++-------- frontend/web/styles/project/_forms.scss | 4 + frontend/web/styles/project/_modals.scss | 7 +- 13 files changed, 234 insertions(+), 233 deletions(-) rename frontend/web/components/{pages/FeatureHistoryPage.tsx => FeatureHistory.tsx} (72%) diff --git a/frontend/e2e/helpers.cafe.ts b/frontend/e2e/helpers.cafe.ts index 8b63eabba38c..eb92aac91684 100644 --- a/frontend/e2e/helpers.cafe.ts +++ b/frontend/e2e/helpers.cafe.ts @@ -264,9 +264,8 @@ export const logout = async (t) => { } export const goToFeatureVersions = async (featureIndex:number) =>{ - await gotoFeatures() - await click(byId(`feature-action-${featureIndex}`)) - await click(byId(`feature-history-${featureIndex}`)) + await gotoFeature(featureIndex) + await click(byId('change-history')) } export const compareVersion = async ( @@ -294,10 +293,12 @@ export const compareVersion = async ( if(newValue) { await assertTextContent(byId(`old-value`), `${oldValue}`) } + await closeModal() } export const assertNumberOfVersions = async (index:number, versions:number) =>{ await goToFeatureVersions(index) await waitForElementVisible(byId(`history-item-${versions-2}-compare`)) + await closeModal() } export const createRemoteConfig = async ( diff --git a/frontend/web/components/pages/FeatureHistoryPage.tsx b/frontend/web/components/FeatureHistory.tsx similarity index 72% rename from frontend/web/components/pages/FeatureHistoryPage.tsx rename to frontend/web/components/FeatureHistory.tsx index d6a8abb319d3..1e6bc12bff67 100644 --- a/frontend/web/components/pages/FeatureHistoryPage.tsx +++ b/frontend/web/components/FeatureHistory.tsx @@ -1,50 +1,33 @@ import React, { FC, useState } from 'react' -import FlagSelect from 'components/FlagSelect' import ConfigProvider from 'common/providers/ConfigProvider' -import { RouterChildContext } from 'react-router' -import Utils from 'common/utils/utils' -import ProjectStore from 'common/stores/project-store' import { useGetFeatureVersionsQuery } from 'common/services/useFeatureVersion' import { useGetUsersQuery } from 'common/services/useUser' import AccountStore from 'common/stores/account-store' -import PanelSearch from 'components/PanelSearch' -import { - Environment, - FeatureVersion as TFeatureVersion, -} from 'common/types/responses' -import PageTitle from 'components/PageTitle' -import Button from 'components/base/forms/Button' -import FeatureVersion from 'components/FeatureVersion' -import InlineModal from 'components/InlineModal' -import TableFilterItem from 'components/tables/TableFilterItem' +import { FeatureVersion as TFeatureVersion } from 'common/types/responses' +import Button from './base/forms/Button' +import FeatureVersion from './FeatureVersion' +import InlineModal from './InlineModal' +import TableFilterItem from './tables/TableFilterItem' import moment from 'moment' -import { Link } from 'react-router-dom' -import DateList from 'components/DateList' +import DateList from './DateList' +import PlanBasedBanner from './PlanBasedAccess' import classNames from 'classnames' -import PlanBasedBanner from 'components/PlanBasedAccess' const widths = [250, 150] type FeatureHistoryPageType = { - router: RouterChildContext['router'] - - match: { - params: { - environmentId: string - projectId: string - } - } + environmentId: string + environmentApiKey: string + projectId: string + feature: number } -const FeatureHistoryPage: FC = ({ match, router }) => { - const feature = Utils.fromParam(router.route.location.search)?.feature +const FeatureHistory: FC = ({ + environmentApiKey, + environmentId, + feature, + projectId, +}) => { const [open, setOpen] = useState(false) - - const env: Environment | undefined = ProjectStore.getEnvironment( - match.params.environmentId, - ) as any - // @ts-ignore - const environmentId = `${env?.id}` - const environmentApiKey = `${env?.api_key}` const { data: users } = useGetUsersQuery({ organisationId: AccountStore.getOrganisation().id, }) @@ -56,7 +39,7 @@ const FeatureHistoryPage: FC = ({ match, router }) => { is_live: true, page, }, - { skip: !env || !feature }, + { skip: !environmentId || !feature }, ) const [selected, setSelected] = useState(null) const live = data?.results?.[0] @@ -64,41 +47,21 @@ const FeatureHistoryPage: FC = ({ match, router }) => { const [diff, setDiff] = useState(null) const versionLimit = 3 return ( -
- -
- View and rollback history of feature values, multivariate values and - segment overrides. -
-
-
-
-
- -
- { - router.history.replace( - `${document.location.pathname}?feature=${flagId}`, - ) - }} - value={feature ? parseInt(feature) : null} - /> -
-
-
+
+
Change History
+
+ View and rollback history of feature values, multivariate values and + segment overrides.
- {!!versionLimit && ( - - )} + {/*{!!versionLimit && (*/} + {/* */} + {/*)}*/} items={data} isLoading={isLoading} @@ -106,7 +69,7 @@ const FeatureHistoryPage: FC = ({ match, router }) => { prevPage={() => setPage(page + 1)} goToPage={setPage} renderRow={(v: TFeatureVersion, i: number) => { - const isOverLimit = !!versionLimit && i + 1 > versionLimit + const isOverLimit = false const user = users?.find((user) => v.published_by === user.id) return ( @@ -115,7 +78,11 @@ const FeatureHistoryPage: FC = ({ match, router }) => { 'blur no-pointer': isOverLimit, })} > -
+
= ({ match, router }) => {
- - +
{i + 1 !== data!.results.length && ( @@ -229,7 +198,7 @@ const FeatureHistoryPage: FC = ({ match, router }) => {
{diff === v.uuid && ( = ({ match, router }) => { ) } -export default ConfigProvider(FeatureHistoryPage) +export default ConfigProvider(FeatureHistory) diff --git a/frontend/web/components/FeatureRow.js b/frontend/web/components/FeatureRow.js index a878bfdf873b..5c609d44e63f 100644 --- a/frontend/web/components/FeatureRow.js +++ b/frontend/web/components/FeatureRow.js @@ -390,9 +390,7 @@ class TheComponent extends Component { hideHistory={!environment?.use_v2_feature_versioning} onShowHistory={() => { if (disableControls) return - this.context.router.history.push( - `/project/${projectId}/environment/${environmentId}/history?feature=${projectFlag.id}`, - ) + this.editFeature(projectFlag, environmentFlags[id], 'history') }} onShowAudit={() => { if (disableControls) return diff --git a/frontend/web/components/base/forms/TabItem.tsx b/frontend/web/components/base/forms/TabItem.tsx index 89a0028eccac..ac303713eb44 100644 --- a/frontend/web/components/base/forms/TabItem.tsx +++ b/frontend/web/components/base/forms/TabItem.tsx @@ -4,6 +4,7 @@ type TabItemType = { tabLabelString?: string tabLabel: ReactNode children: ReactNode + className?: string } const TabItem: FC = ({ children }) => { diff --git a/frontend/web/components/base/forms/Tabs.js b/frontend/web/components/base/forms/Tabs.js index 3befc06b976d..9644607baf75 100644 --- a/frontend/web/components/base/forms/Tabs.js +++ b/frontend/web/components/base/forms/Tabs.js @@ -41,7 +41,7 @@ const Tabs = class extends React.Component { return (
{!hideNav && @@ -97,7 +97,7 @@ const Tabs = class extends React.Component { key={`content${i}`} className={`tab-item ${isSelected ? ' tab-active' : ''} ${ this.props.isRoles && 'p-0' - }`} + } ${child.props.className || ''}`} > {child}
diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx index 24f132903457..b3ef1810e64e 100644 --- a/frontend/web/components/diff/DiffFeature.tsx +++ b/frontend/web/components/diff/DiffFeature.tsx @@ -76,7 +76,7 @@ const DiffFeature: FC = ({ const hideValue = !totalChanges && (diff.newValue === null || diff.newValue === undefined) return ( -
+
{!feature ? (
@@ -94,6 +94,7 @@ const DiffFeature: FC = ({ value={value} > Value @@ -105,8 +106,8 @@ const DiffFeature: FC = ({ } > {!totalChanges && ( -
- No Changes Found +
+ No Changes Found
)} {!!valueConflict && ( @@ -169,6 +170,7 @@ const DiffFeature: FC = ({ {!!variationDiffs?.diffs?.length && ( Variations{' '} @@ -185,6 +187,7 @@ const DiffFeature: FC = ({ )} {!!segmentDiffs?.diffs.length && ( Segment Overrides diff --git a/frontend/web/components/diff/DiffSegments.tsx b/frontend/web/components/diff/DiffSegments.tsx index ab6784595812..98c56006651f 100644 --- a/frontend/web/components/diff/DiffSegments.tsx +++ b/frontend/web/components/diff/DiffSegments.tsx @@ -116,6 +116,7 @@ const DiffSegments: FC = ({ {!!created.length && ( Created
{created.length}
@@ -135,6 +136,7 @@ const DiffSegments: FC = ({ )} {!!deleted.length && ( Deleted
{deleted.length}
@@ -154,6 +156,7 @@ const DiffSegments: FC = ({ )} {!!modified.length && ( Modified
{modified.length}
@@ -173,6 +176,7 @@ const DiffSegments: FC = ({ )} {!!unChanged.length && ( Unchanged
{unChanged.length}
diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 1d02c20cce8b..933eafda85e9 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -42,6 +42,7 @@ import { removeUserOverride } from 'components/RemoveUserOverride' import ExternalResourcesLinkTab from 'components/ExternalResourcesLinkTab' import { saveFeatureWithValidation } from 'components/saveFeatureWithValidation' import PlanBasedBanner from 'components/PlanBasedAccess' +import FeatureHistory from 'components/FeatureHistory' const CreateFlag = class extends Component { static displayName = 'CreateFlag' @@ -1021,7 +1022,11 @@ const CreateFlag = class extends Component { project.total_features, project.max_features_allowed, ) - + const showIdentityOverrides = + !identity && + isEdit && + !existingChangeRequest && + !hideIdentityOverridesTab return ( - - Identity overrides override feature - values for individual identities. The - overrides take priority over an segment - overrides and environment defaults. - Identity overrides will only apply when - you identify via the SDK.{' '} - - Check the Docs for more details - - . - - + - Identity Overrides{' '} - - - } - place='top' - > - { - Constants.strings - .IDENTITY_OVERRIDES_DESCRIPTION - } - + <> + + Identity Overrides{' '} + + + } + place='top' + > + { + Constants.strings + .IDENTITY_OVERRIDES_DESCRIPTION + } + +
+ + Identity overrides override + feature values for individual + identities. The overrides take + priority over an segment + overrides and environment + defaults. Identity overrides + will only apply when you + identify via the SDK.{' '} + + Check the Docs for more + details + + . + +
+ } action={ !Utils.getIsEdge() && ( @@ -1681,40 +1693,51 @@ const CreateFlag = class extends Component {
)} {!existingChangeRequest && - !Project.disableAnalytics && - this.props.flagId && ( + this.props.flagId && + (isVersioned || + !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 - - +
)} + {!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 + + +
+ )} {Utils.getFlagsmithHasFeature( 'github_integration', ) && diff --git a/frontend/web/components/pages/FeatureHistoryDetailPage.tsx b/frontend/web/components/pages/FeatureHistoryDetailPage.tsx index 3d29f9067005..c0360f47c61c 100644 --- a/frontend/web/components/pages/FeatureHistoryDetailPage.tsx +++ b/frontend/web/components/pages/FeatureHistoryDetailPage.tsx @@ -15,6 +15,8 @@ import moment from 'moment' import ErrorMessage from 'components/ErrorMessage' import Tabs from 'components/base/forms/Tabs' import TabItem from 'components/base/forms/TabItem' +import Breadcrumb from 'components/Breadcrumb' +import { useGetProjectFlagQuery } from 'common/services/useProjectFlag' type FeatureHistoryPageType = { router: RouterChildContext['router'] @@ -60,8 +62,27 @@ const FeatureHistoryPage: FC = ({ match, router }) => { ) const user = users?.find((user) => data?.published_by === user.id) const live = versions?.results?.[0] + const { data: feature } = useGetProjectFlagQuery( + { id: `${data?.feature}`, project: match.params.projectId }, + { + skip: !data?.feature, + }, + ) return (
+
View and rollback history of feature values, multivariate values and diff --git a/frontend/web/components/pages/HomeAside.tsx b/frontend/web/components/pages/HomeAside.tsx index 4de009213b6e..37346ee4943a 100644 --- a/frontend/web/components/pages/HomeAside.tsx +++ b/frontend/web/components/pages/HomeAside.tsx @@ -215,18 +215,6 @@ const HomeAside: FC = ({ ) : null} - {environment.use_v2_feature_versioning && ( - - - - - History - - )} {Utils.renderWithPermission( manageIdentityPermission, Constants.environmentPermissions( diff --git a/frontend/web/routes.js b/frontend/web/routes.js index 24a113eb7645..9a310e5c5b41 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -29,7 +29,6 @@ import WidgetPage from './components/pages/WidgetPage' import BrokenPage from './components/pages/BrokenPage' import GitHubSetupPage from './components/pages/GitHubSetupPage' import AuditLogItemPage from './components/pages/AuditLogItemPage' -import FeatureHistoryPage from './components/pages/FeatureHistoryPage' import Utils from 'common/utils/utils' import ProjectsPage from './components/ProjectsPage' import OrganisationSettingsRedirectPage from './components/pages/OrganisationSettingsRedirectPage' @@ -42,81 +41,81 @@ import { ParameterizedRoute } from './components/base/higher-order/Parameterized import FeatureHistoryDetailPage from './components/pages/FeatureHistoryDetailPage' export const routes = { - 'root': '/', - 'login': '/login', - 'not-found': '/404', - 'signup': '/signup', - 'home': '/home', - 'github-setup': '/github-setup', - 'maintenance': '/maintenance', - 'password-reset': '/password-reset/confirm/:uid/:token/', 'features': '/project/:projectId/environment/:environmentId/features', 'change-requests': '/project/:projectId/environment/:environmentId/change-requests', - 'scheduled-changes': - '/project/:projectId/environment/:environmentId/scheduled-changes', + 'github-setup': '/github-setup', 'change-request': '/project/:projectId/environment/:environmentId/change-requests/:id', - 'scheduled-change': - '/project/:projectId/environment/:environmentId/scheduled-changes/:id', - 'widget': '/widget', + 'home': '/home', + 'login': '/login', + 'maintenance': '/maintenance', 'invite': '/invite/:id', - 'invite-link': '/invite-link/:id', + 'not-found': '/404', 'broken': '/broken', - 'oauth': '/oauth/:type', - 'saml': '/saml', + 'root': '/', + 'invite-link': '/invite-link/:id', 'environment-settings': '/project/:projectId/environment/:environmentId/settings', - 'sdk-keys': '/project/:projectId/environment/:environmentId/sdk-keys', + 'signup': '/signup', 'integrations': '/project/:projectId/integrations', - 'users': '/project/:projectId/environment/:environmentId/users', - 'user-id': '/project/:projectId/environment/:environmentId/users/:identity', - 'user': '/project/:projectId/environment/:environmentId/users/:identity/:id', + 'oauth': '/oauth/:type', + 'password-reset': '/password-reset/confirm/:uid/:token/', 'create-environment': '/project/:projectId/environment/create', - 'project-settings-in-environment': - '/project/:projectId/environment/:environmentId/project-settings', + 'scheduled-change': + '/project/:projectId/environment/:environmentId/scheduled-changes/:id', 'compare': '/project/:projectId/compare', + 'scheduled-changes': + '/project/:projectId/environment/:environmentId/scheduled-changes', 'feature-history': '/project/:projectId/environment/:environmentId/history', 'feature-history-detail': '/project/:projectId/environment/:environmentId/history/:id/', - 'project-settings': '/project/:projectId/settings', - 'permissions': '/project/:projectId/permissions', - 'segments': '/project/:projectId/segments', + 'widget': '/widget', 'organisation-settings': '/organisation/:organisationId/settings', 'organisation-permissions': '/organisation/:organisationId/permissions', - 'organisation-usage': '/organisation/:organisationId/usage', + 'saml': '/saml', 'organisation-settings-redirect': '/organisation-settings', - 'organisation-projects': '/organisation/:organisationId/projects', + 'sdk-keys': '/project/:projectId/environment/:environmentId/sdk-keys', 'account-settings': '/project/:projectId/environment/:environmentId/account', + 'user-id': '/project/:projectId/environment/:environmentId/users/:identity', 'audit-log': '/project/:projectId/audit-log', - 'organisations': '/organisations', + 'users': '/project/:projectId/environment/:environmentId/users', 'audit-log-item': '/project/:projectId/audit-log/:id', + 'user': '/project/:projectId/environment/:environmentId/users/:identity/:id', 'create-organisation': '/create', - 'project-redirect': '/project/:projectId', + 'project-settings-in-environment': + '/project/:projectId/environment/:environmentId/project-settings', 'account': '/account', + 'permissions': '/project/:projectId/permissions', + 'organisation-projects': '/organisation/:organisationId/projects', + 'project-settings': '/project/:projectId/settings', + 'organisation-usage': '/organisation/:organisationId/usage', + 'segments': '/project/:projectId/segments', + 'organisations': '/organisations', + 'project-redirect': '/project/:projectId', } export default ( - - + + - - + + {Utils.getFlagsmithHasFeature('github_integration') && ( - + )} - + - + - - + + - - - + + + - + - + - - + @@ -240,17 +230,13 @@ export default ( exact component={ProjectRedirectPage} /> - + - +