diff --git a/frontend/common/services/useProjectFlag.ts b/frontend/common/services/useProjectFlag.ts index 40e93ad4c780..271833800370 100644 --- a/frontend/common/services/useProjectFlag.ts +++ b/frontend/common/services/useProjectFlag.ts @@ -9,7 +9,7 @@ import { sortMultivariateOptions } from 'common/utils/multivariate' */ export const FEATURES_PAGE_SIZE = 50 -function recursivePageGet( +export function recursivePageGet( url: string, parentRes: null | PagedResponse, baseQuery: (arg: unknown) => any, // matches rtk types, diff --git a/frontend/web/components/CompareEnvironments.js b/frontend/web/components/CompareEnvironments.js deleted file mode 100644 index 4ec721e67d8b..000000000000 --- a/frontend/web/components/CompareEnvironments.js +++ /dev/null @@ -1,500 +0,0 @@ -// import propTypes from 'prop-types'; -import React, { Component } from 'react' -import each from 'lodash/each' -import sortBy from 'lodash/sortBy' -import keyBy from 'lodash/keyBy' -import EnvironmentSelect from './EnvironmentSelect' -import data from 'common/data/base/_data' -import ProjectStore from 'common/stores/project-store' -import FeatureRow from './feature-summary/FeatureRow' -import FeatureListStore from 'common/stores/feature-list-store' -import ConfigProvider from 'common/providers/ConfigProvider' -import Permission from 'common/providers/Permission' -import Tag from './tags/Tag' -import Icon from './icons/Icon' -import Constants from 'common/constants' -import Button from './base/forms/Button' -import EmptyState from './EmptyState' -import Tooltip from './Tooltip' -import { withRouter } from 'react-router-dom' -import { getDarkMode } from 'project/darkMode' -import { getStore } from 'common/store' -import { removeProjectFlag } from 'common/services/useProjectFlag' -import { hasMultivariateChange } from 'common/utils/compareMultivariate' - -const featureNameWidth = 300 - -class CompareEnvironments extends Component { - static displayName = 'CompareEnvironments' - - static propTypes = {} - - constructor(props) { - super(props) - this.state = { - environmentLeft: props.environmentId, - environmentRight: '', - isLoading: true, - projectFlagsLeft: null, - projectFlagsRight: null, - showArchived: false, - } - } - - componentDidUpdate(prevProps, prevState) { - if ( - this.state.environmentLeft !== prevState.environmentLeft || - this.state.environmentRight !== prevState.environmentRight - ) { - this.fetch() - } - } - - fetch = () => { - if (!this.state.environmentLeft || !this.state.environmentRight) { - return - } - this.setState({ isLoading: true }) - return Promise.all([ - data.get( - `${Project.api}projects/${ - this.props.projectId - }/features/?page_size=999&environment=${ProjectStore.getEnvironmentIdFromKey( - this.state.environmentLeft, - )}`, - ), - data.get( - `${Project.api}projects/${ - this.props.projectId - }/features/?page_size=999&environment=${ProjectStore.getEnvironmentIdFromKey( - this.state.environmentRight, - )}`, - ), - data.get( - `${Project.api}environments/${this.state.environmentLeft}/featurestates/?page_size=999`, - ), - data.get( - `${Project.api}environments/${this.state.environmentRight}/featurestates/?page_size=999`, - ), - ]) - .then( - ([ - environmentLeftProjectFlags, - environmentRightProjectFlags, - environmentLeftFlags, - environmentRightFlags, - ]) => { - const changes = [] - const same = [] - each( - sortBy(environmentLeftProjectFlags.results, (p) => p.name), - (projectFlagLeft) => { - const projectFlagRight = - environmentRightProjectFlags.results?.find( - (projectFlagRight) => - projectFlagRight.id === projectFlagLeft.id, - ) - const leftSide = environmentLeftFlags.results.find( - (v) => v.feature === projectFlagLeft.id, - ) - const rightSide = environmentRightFlags.results.find( - (v) => v.feature === projectFlagLeft.id, - ) - const change = { - leftEnabled: leftSide.enabled, - leftEnvironmentFlag: leftSide, - leftValue: leftSide.feature_state_value, - projectFlagLeft, - projectFlagRight, - rightEnabled: rightSide.enabled, - rightEnvironmentFlag: rightSide, - rightValue: rightSide.feature_state_value, - } - change.enabledChanged = change.rightEnabled !== change.leftEnabled - change.valueChanged = change.rightValue !== change.leftValue - change.multivariateChanged = hasMultivariateChange( - leftSide, - rightSide, - ) - if ( - change.enabledChanged || - change.valueChanged || - change.multivariateChanged || - projectFlagLeft.num_identity_overrides || - projectFlagLeft.num_segment_overrides || - projectFlagRight.num_identity_overrides || - projectFlagRight.num_segment_overrides - ) { - changes.push(change) - } else { - same.push(change) - } - }, - ) - this.setState({ - changes, - environmentLeftFlags: keyBy( - environmentLeftFlags.results, - 'feature', - ), - environmentRightFlags: keyBy( - environmentRightFlags.results, - 'feature', - ), - isLoading: false, - projectFlagsLeft: environmentLeftProjectFlags.results, - projectFlagsRight: environmentLeftProjectFlags.results, - same, - }) - }, - ) - .catch(() => { - this.setState({ isLoading: false }) - }) - } - - onSave = () => this.fetch() - - filter = (items) => { - if (!items) return items - - return items.filter((v) => { - if (this.state.showArchived) { - return true - } - return !v.projectFlagLeft.is_archived - }) - } - - render() { - return ( -
-
-
Compare Environments
-

- Compare feature flag changes across environments. -

-
- - -
- - this.setState({ environmentLeft }) - } - value={this.state.environmentLeft} - /> -
- -
- -
- -
- - this.setState({ environmentRight }) - } - value={this.state.environmentRight} - /> -
-
-
- - {this.state.environmentLeft && this.state.environmentRight ? ( - - {({}, { toggleFlag }) => { - // Adapt old FeatureListProvider signatures to new FeatureRow signatures - const adaptedToggleFlag = - (environmentId) => (projectFlag, environmentFlag, onError) => { - toggleFlag( - this.props.projectId, - environmentId, - projectFlag, - environmentFlag, - onError, - ) - } - const adaptedRemoveFlag = (projectFlag) => { - removeProjectFlag(getStore(), { - flag_id: projectFlag.id, - project_id: this.props.projectId, - }) - } - const renderRow = (p, i, fadeEnabled, fadeValue) => { - const environmentLeft = ProjectStore.getEnvironment( - this.state.environmentLeft, - ) - const environmentRight = ProjectStore.getEnvironment( - this.state.environmentRight, - ) - return ( - -
- -
- {p.projectFlagLeft.name} -
- - - } - > - {p.projectFlagLeft.description} -
-
- - {({ permission }) => ( - - )} - - - {({ permission }) => ( - - )} - -
- ) - } - - return ( -
- {this.state.isLoading && ( -
- -
- )} - {!this.state.isLoading && - (!this.state.changes || !this.state.changes.length) && ( - - )} - {!this.state.isLoading && - this.state.changes && - !!this.state.changes.length && ( -
- - { - FeatureListStore.isLoading = true - this.setState({ - showArchived: !this.state.showArchived, - }) - }} - className='px-2 py-2 ml-2 mr-2' - tag={Constants.archivedTag} - /> - - } - header={ - -
- Name -
- -
- { - ProjectStore.getEnvironment( - this.state.environmentLeft, - ).name - } -
- -
- -
- { - ProjectStore.getEnvironment( - this.state.environmentRight, - ).name - } -
- -
-
- } - className='no-pad mt-2' - items={this.filter(this.state.changes)} - renderRow={(p, i) => - renderRow( - p, - i, - !p.enabledChanged, - !(p.valueChanged || p.multivariateChanged), - ) - } - /> -
- )} - {!this.state.isLoading && - this.state.same && - !!this.state.same.length && ( -
-
- -
- Name -
- -
- { - ProjectStore.getEnvironment( - this.state.environmentLeft, - ).name - } -
- Value -
- -
- { - ProjectStore.getEnvironment( - this.state.environmentRight, - ).name - } -
- Value -
- - } - className='no-pad mt-2' - items={this.filter(this.state.same)} - renderRow={(p, i) => - renderRow( - p, - i, - !p.enabledChanged, - !(p.valueChanged || p.multivariateChanged), - ) - } - /> -
-
- )} -
- ) - }} -
- ) : ( - '' - )} -
- ) - } -} - -export default withRouter(ConfigProvider(CompareEnvironments)) diff --git a/frontend/web/components/CompareEnvironments/CompareEnvironments.tsx b/frontend/web/components/CompareEnvironments/CompareEnvironments.tsx new file mode 100644 index 000000000000..504ed3def0f2 --- /dev/null +++ b/frontend/web/components/CompareEnvironments/CompareEnvironments.tsx @@ -0,0 +1,334 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react' +import sortBy from 'lodash/sortBy' +import { useHistory } from 'react-router-dom' +import EnvironmentSelect from 'components/EnvironmentSelect' +import Icon from 'components/icons/Icon' +import Panel from 'components/base/grid/Panel' +import Button from 'components/base/forms/Button' +import CreateFlagModal from 'components/modals/create-feature' +import { FeaturesTableFilters } from 'components/pages/features/components' +import Utils from 'common/utils/utils' +import { TagStrategy } from 'common/types/responses' +import type { FilterState } from 'common/types/featureFilters' +import { SortOrder } from 'common/types/requests' +import { + getFiltersFromParams, + hasActiveFilters, +} from 'common/utils/featureFilterParams' +import CompareFeatureRow, { EditFeatureHandler } from './CompareFeatureRow' +import { FeatureChange } from './types' +import { useEnvironmentComparison } from './useEnvironmentComparison' + +type CompareEnvironmentsProps = { + projectId: string + environmentId?: string +} + +const filterAndSortChanges = ( + items: FeatureChange[] | null, + filters: FilterState, +): FeatureChange[] => { + if (!items) return [] + + const filtered = items.filter((item) => { + if (!filters.showArchived && item.projectFlagLeft.is_archived) { + return false + } + + if (filters.search) { + const searchLower = filters.search.toLowerCase() + if (!item.projectFlagLeft.name.toLowerCase().includes(searchLower)) { + return false + } + } + + if (filters.tags.length > 0) { + const featureTags = item.projectFlagLeft.tags || [] + + if (filters.tags.includes('')) { + if (featureTags.length > 0) { + return false + } + } else { + const tagIds = filters.tags.filter((t) => t !== '') as number[] + if (tagIds.length > 0) { + if (filters.tag_strategy === TagStrategy.INTERSECTION) { + if (!tagIds.every((tagId) => featureTags.includes(tagId))) { + return false + } + } else if (!tagIds.some((tagId) => featureTags.includes(tagId))) { + return false + } + } + } + } + + if (filters.owners.length > 0) { + const ownerIds = item.projectFlagLeft.owners?.map((o) => o.id) || [] + if (!filters.owners.some((id) => ownerIds.includes(id))) { + return false + } + } + + if (filters.group_owners.length > 0) { + const groupIds = item.projectFlagLeft.group_owners?.map((g) => g.id) || [] + if (!filters.group_owners.some((id) => groupIds.includes(id))) { + return false + } + } + + if ( + filters.is_enabled !== null && + item.leftEnabled !== filters.is_enabled + ) { + return false + } + + return true + }) + + const sorted = + filters.sort.sortBy === 'created_date' + ? sortBy(filtered, (f) => f.projectFlagLeft.created_date) + : sortBy(filtered, (f) => f.projectFlagLeft.name.toLowerCase()) + + return filters.sort.sortOrder === SortOrder.DESC ? sorted.reverse() : sorted +} + +const CompareEnvironments: FC = ({ + environmentId: initialEnvironmentId, + projectId, +}) => { + const history = useHistory() + const [environmentLeft, setEnvironmentLeft] = useState( + initialEnvironmentId || '', + ) + const [environmentRight, setEnvironmentRight] = useState('') + const [expandedRows, setExpandedRows] = useState>(new Set()) + const [filters, setFilters] = useState(getFiltersFromParams({})) + + const projectIdNum = parseInt(projectId) + + const { + changes, + error, + isLoading, + leftEnvironment, + leftEnvironmentId, + refresh, + rightEnvironment, + rightEnvironmentId, + } = useEnvironmentComparison({ + leftEnvironmentKey: environmentLeft, + projectId, + rightEnvironmentKey: environmentRight, + }) + + // Collapse any expanded rows whenever a fresh comparison loads + useEffect(() => { + setExpandedRows(new Set()) + }, [changes]) + + const editFeature: EditFeatureHandler = useCallback( + (projectFlag, environmentFlag, environmentKey, environmentName) => { + openModal( + + + Edit Feature: {projectFlag.name} + {environmentName && ( + ({environmentName}) + )} + + + , + , + 'side-modal create-feature-modal', + () => { + refresh() + }, + ) + }, + [history, projectIdNum, refresh], + ) + + const filteredItems = useMemo( + () => filterAndSortChanges(changes, filters), + [changes, filters], + ) + + const differentItems = useMemo( + () => filteredItems.filter((item) => item.hasDiff), + [filteredItems], + ) + + const sameItems = useMemo( + () => filteredItems.filter((item) => !item.hasDiff), + [filteredItems], + ) + + const toggleExpand = (featureId: number) => { + setExpandedRows((prev) => { + const next = new Set(prev) + if (next.has(featureId)) { + next.delete(featureId) + } else { + next.add(featureId) + } + return next + }) + } + + const handleFilterChange = (updates: Partial) => { + setFilters((prev) => ({ ...prev, ...updates })) + } + + const clearFilters = () => { + setFilters(getFiltersFromParams({})) + } + + const hasFilters = hasActiveFilters(filters) + + const renderRow = (item: FeatureChange, index: number) => ( + + ) + + const renderResults = () => { + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+ Could not load the comparison.{' '} + +
+ ) + } + + return ( + <> + +
+ + {renderTableHeader('Changed features', differentItems.length)} + {differentItems.length > 0 ? ( + differentItems.map((item, i) => renderRow(item, i)) + ) : ( +
+ No differences found +
+ )} +
+
+ + {sameItems.length > 0 && ( + +
+ {renderTableHeader('Unchanged features', sameItems.length)} + {sameItems.map((item, i) => renderRow(item, i))} +
+
+ )} + + ) + } + + const renderTableHeader = (label: string, count: number) => ( + +
+ {label} + {count} +
+
+ {leftEnvironment?.name} +
+
+ {rightEnvironment?.name} +
+
+ Segment changes +
+
+ ) + + return ( +
+
+
Compare Environments
+

+ Compare feature flag configurations across environments. +

+
+ + +
+ setEnvironmentLeft(value as string)} + value={environmentLeft} + /> +
+ +
+ +
+ +
+ setEnvironmentRight(value as string)} + value={environmentRight} + /> +
+
+ + {environmentLeft && environmentRight && renderResults()} +
+ ) +} + +export default CompareEnvironments diff --git a/frontend/web/components/CompareEnvironments/CompareFeatureRow.tsx b/frontend/web/components/CompareEnvironments/CompareFeatureRow.tsx new file mode 100644 index 000000000000..0741e32636ab --- /dev/null +++ b/frontend/web/components/CompareEnvironments/CompareFeatureRow.tsx @@ -0,0 +1,157 @@ +import React, { FC } from 'react' +import { FeatureState, ProjectFlag } from 'common/types/responses' +import Icon from 'components/icons/Icon' +import Switch from 'components/Switch' +import FeatureName from 'components/feature-summary/FeatureName' +import FeatureValue from 'components/feature-summary/FeatureValue' +import SegmentsIcon from 'components/icons/SegmentsIcon' +import Button from 'components/base/forms/Button' +import ExpandedRow from './ExpandedRow' +import { FeatureChange } from './types' + +export type EditFeatureHandler = ( + projectFlag: ProjectFlag, + environmentFlag: FeatureState, + environmentKey: string, + environmentName?: string, +) => void + +type CompareFeatureRowProps = { + item: FeatureChange + index: number + projectId: string + isExpanded: boolean + onToggle: (featureId: number) => void + onEdit: EditFeatureHandler + leftEnvironmentKey: string + rightEnvironmentKey: string + leftEnvironmentId?: number + rightEnvironmentId?: number + leftEnvironmentName?: string + rightEnvironmentName?: string +} + +const CompareFeatureRow: FC = ({ + index, + isExpanded, + item, + leftEnvironmentId, + leftEnvironmentKey, + leftEnvironmentName, + onEdit, + onToggle, + projectId, + rightEnvironmentId, + rightEnvironmentKey, + rightEnvironmentName, +}) => { + const featureId = item.projectFlagLeft.id + const totalSegments = Math.max( + item.projectFlagLeft.num_segment_overrides || 0, + item.projectFlagRight?.num_segment_overrides || 0, + ) + const toggle = () => onToggle(featureId) + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + toggle() + } + }} + > +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ {totalSegments > 0 && ( + + + {totalSegments} + + )} +
+
+ + {isExpanded && leftEnvironmentId && rightEnvironmentId && ( + + )} +
+ ) +} + +export default CompareFeatureRow diff --git a/frontend/web/components/CompareEnvironments/ExpandedRow.tsx b/frontend/web/components/CompareEnvironments/ExpandedRow.tsx new file mode 100644 index 000000000000..3c63bd84bb24 --- /dev/null +++ b/frontend/web/components/CompareEnvironments/ExpandedRow.tsx @@ -0,0 +1,66 @@ +import React, { FC } from 'react' +import { useGetFeatureStatesQuery } from 'common/services/useFeatureState' +import { FeatureStateWithConflict } from 'common/types/responses' +import DiffFeature from 'components/diff/DiffFeature' +import { FeatureChange } from './types' + +type ExpandedRowProps = { + item: FeatureChange + projectId: string + environmentLeftId: number + environmentRightId: number + oldEnvName?: string + newEnvName?: string +} + +const ExpandedRow: FC = ({ + environmentLeftId, + environmentRightId, + item, + newEnvName, + oldEnvName, + projectId, +}) => { + const { data: leftStates, isLoading: leftLoading } = useGetFeatureStatesQuery( + { + environment: environmentLeftId, + feature: item.projectFlagLeft.id, + }, + ) + + const { data: rightStates, isLoading: rightLoading } = + useGetFeatureStatesQuery({ + environment: environmentRightId, + feature: item.projectFlagLeft.id, + }) + + if (leftLoading || rightLoading) { + return ( +
+ +
+ ) + } + + return ( +
+ +
+ ) +} + +export default ExpandedRow diff --git a/frontend/web/components/CompareEnvironments/index.ts b/frontend/web/components/CompareEnvironments/index.ts new file mode 100644 index 000000000000..fd2619d8f844 --- /dev/null +++ b/frontend/web/components/CompareEnvironments/index.ts @@ -0,0 +1,2 @@ +export { default } from './CompareEnvironments' +export type { FeatureChange } from './types' diff --git a/frontend/web/components/CompareEnvironments/types.ts b/frontend/web/components/CompareEnvironments/types.ts new file mode 100644 index 000000000000..9454302d7766 --- /dev/null +++ b/frontend/web/components/CompareEnvironments/types.ts @@ -0,0 +1,20 @@ +import { + FeatureState, + FlagsmithValue, + ProjectFlag, +} from 'common/types/responses' + +export type FeatureChange = { + leftEnabled: boolean + leftValue: FlagsmithValue + leftEnvironmentFlag: FeatureState + rightEnabled: boolean + rightValue: FlagsmithValue + rightEnvironmentFlag: FeatureState + projectFlagLeft: ProjectFlag + projectFlagRight?: ProjectFlag + enabledChanged: boolean + valueChanged: boolean + multivariateChanged: boolean + hasDiff: boolean +} diff --git a/frontend/web/components/CompareEnvironments/useEnvironmentComparison.ts b/frontend/web/components/CompareEnvironments/useEnvironmentComparison.ts new file mode 100644 index 000000000000..ca67d3f2a8bc --- /dev/null +++ b/frontend/web/components/CompareEnvironments/useEnvironmentComparison.ts @@ -0,0 +1,165 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import sortBy from 'lodash/sortBy' +import data from 'common/data/base/_data' +import Project from 'common/project' +import ProjectStore from 'common/stores/project-store' +import { hasMultivariateChange } from 'common/utils/compareMultivariate' +import { recursivePageGet } from 'common/services/useProjectFlag' +import { + Environment, + FeatureState, + PagedResponse, + ProjectFlag, +} from 'common/types/responses' +import { FeatureChange } from './types' + +type UseEnvironmentComparisonArgs = { + projectId: string + leftEnvironmentKey: string + rightEnvironmentKey: string +} + +// Adapt _data.get to the baseQuery contract recursivePageGet expects: it reads +// `results`/`next` straight off the resolved body, which is what _data.get returns +const pagedGet = (url: string) => + recursivePageGet(url, null, (arg) => + data.get((arg as { url: string }).url), + ) as Promise> + +const deriveChanges = ( + leftProjectFlags: ProjectFlag[], + rightProjectFlags: ProjectFlag[], + leftFlags: FeatureState[], + rightFlags: FeatureState[], +): FeatureChange[] => { + const changes: FeatureChange[] = [] + + sortBy(leftProjectFlags, (p) => p.name).forEach((projectFlagLeft) => { + const projectFlagRight = rightProjectFlags.find( + (pf) => pf.id === projectFlagLeft.id, + ) + const leftSide = leftFlags.find((v) => v.feature === projectFlagLeft.id) + const rightSide = rightFlags.find((v) => v.feature === projectFlagLeft.id) + + if (!leftSide || !rightSide) return + + const enabledChanged = rightSide.enabled !== leftSide.enabled + const valueChanged = + rightSide.feature_state_value !== leftSide.feature_state_value + const multivariateChanged = hasMultivariateChange(leftSide, rightSide) + + const hasDiff = + enabledChanged || + valueChanged || + multivariateChanged || + !!projectFlagLeft.num_segment_overrides || + !!projectFlagRight?.num_segment_overrides + + changes.push({ + enabledChanged, + hasDiff, + leftEnabled: leftSide.enabled, + leftEnvironmentFlag: leftSide, + leftValue: leftSide.feature_state_value, + multivariateChanged, + projectFlagLeft, + projectFlagRight, + rightEnabled: rightSide.enabled, + rightEnvironmentFlag: rightSide, + rightValue: rightSide.feature_state_value, + valueChanged, + }) + }) + + return changes +} + +export const useEnvironmentComparison = ({ + leftEnvironmentKey, + projectId, + rightEnvironmentKey, +}: UseEnvironmentComparisonArgs) => { + const [changes, setChanges] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(false) + + // Bumped on every run so a slow earlier response can't overwrite the + // result of a newer comparison (overlapping-fetch guard) + const requestId = useRef(0) + + const refresh = useCallback(async () => { + if (!leftEnvironmentKey || !rightEnvironmentKey) { + return + } + + const currentRequest = ++requestId.current + setIsLoading(true) + setError(false) + + const leftEnvironmentId = + ProjectStore.getEnvironmentIdFromKey(leftEnvironmentKey) + const rightEnvironmentId = + ProjectStore.getEnvironmentIdFromKey(rightEnvironmentKey) + + try { + const [leftProjectFlags, rightProjectFlags, leftFlags, rightFlags] = + await Promise.all([ + pagedGet( + `${Project.api}projects/${projectId}/features/?environment=${leftEnvironmentId}`, + ), + pagedGet( + `${Project.api}projects/${projectId}/features/?environment=${rightEnvironmentId}`, + ), + pagedGet( + `${Project.api}environments/${leftEnvironmentKey}/featurestates/`, + ), + pagedGet( + `${Project.api}environments/${rightEnvironmentKey}/featurestates/`, + ), + ]) + + if (currentRequest !== requestId.current) return + + setChanges( + deriveChanges( + leftProjectFlags.results || [], + rightProjectFlags.results || [], + leftFlags.results || [], + rightFlags.results || [], + ), + ) + } catch { + if (currentRequest !== requestId.current) return + setError(true) + } finally { + if (currentRequest === requestId.current) { + setIsLoading(false) + } + } + }, [leftEnvironmentKey, rightEnvironmentKey, projectId]) + + useEffect(() => { + refresh() + }, [refresh]) + + const leftEnvironment = ProjectStore.getEnvironment(leftEnvironmentKey) as + | Environment + | undefined + | null + const rightEnvironment = ProjectStore.getEnvironment(rightEnvironmentKey) as + | Environment + | undefined + | null + + return { + changes, + error, + isLoading, + leftEnvironment, + leftEnvironmentId: ProjectStore.getEnvironmentIdFromKey(leftEnvironmentKey), + refresh, + rightEnvironment, + rightEnvironmentId: + ProjectStore.getEnvironmentIdFromKey(rightEnvironmentKey), + } +} diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx index 9713a73e8eeb..6d265c08a234 100644 --- a/frontend/web/components/diff/DiffFeature.tsx +++ b/frontend/web/components/diff/DiffFeature.tsx @@ -30,6 +30,8 @@ type FeatureDiffType = { tabTheme?: string conflicts?: FeatureConflict[] | undefined disableSegments?: boolean + oldEnvName?: string + newEnvName?: string } type ViewMode = 'combined' | 'new' | 'old' const DiffFeature: FC = ({ @@ -37,8 +39,10 @@ const DiffFeature: FC = ({ disableSegments, environmentId, featureId, + newEnvName, newState, noChangesMessage, + oldEnvName, oldState, projectId, tabTheme, @@ -81,8 +85,8 @@ const DiffFeature: FC = ({ !totalChanges && (diff.newValue === null || diff.newValue === undefined) const viewOptions = [ { label: 'Combined Diff', value: 'combined' }, - { label: 'New Value', value: 'new' }, - { label: 'Old Value', value: 'old' }, + { label: newEnvName || 'New Value', value: 'new' }, + { label: oldEnvName || 'Old Value', value: 'old' }, ] const selectedOption = viewOptions.find((v) => v.value === viewMode) return ( @@ -97,6 +101,20 @@ const DiffFeature: FC = ({ !totalSegmentChanges && !totalVariationChanges && noChangesMessage && {noChangesMessage}} + {(oldEnvName || newEnvName) && ( +
+ {oldEnvName && ( + + {oldEnvName} + + )} + {newEnvName && ( + + + {newEnvName} + + )} +
+ )}