diff --git a/app/scripts/modules/core/src/managed/Environments2.less b/app/scripts/modules/core/src/managed/Environments2.less index cee3e6e8749..ece0c162e74 100644 --- a/app/scripts/modules/core/src/managed/Environments2.less +++ b/app/scripts/modules/core/src/managed/Environments2.less @@ -25,6 +25,41 @@ } } } + + .env-direction-btn { + display: flex; + flex-direction: row; + align-items: center; + line-height: 1; + padding: var(--xs-spacing); + background-color: transparent; + color: var(--color-dovegray); + + > i { + margin-top: 1px; + font-size: 16px; + } + + &:focus, + &:active:focus { + outline: none; + } + + @media (max-width: 768px) { + display: none; + } + } + + .environments-list { + width: 100%; + display: grid; + gap: var(--m-spacing); + align-items: flex-start; + + &.side-by-side { + gap: var(--xl-spacing) var(--m-spacing); + } + } } .ui-switcher { diff --git a/app/scripts/modules/core/src/managed/Environments2.tsx b/app/scripts/modules/core/src/managed/Environments2.tsx index 4f7c5ead945..1e6125c0f69 100644 --- a/app/scripts/modules/core/src/managed/Environments2.tsx +++ b/app/scripts/modules/core/src/managed/Environments2.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { HorizontalTabs } from 'core/presentation/horizontalTabs/HorizontalTabs'; +import { EnvironmentsDirectionController } from './environmentBaseElements/EnvironmentsRender'; import { Routes } from './managed.states'; import { useLogEvent } from './utils/logging'; @@ -65,6 +66,7 @@ export const Environments2 = () => {
} onClick={({ title, path }) => { logEvent({ action: `Open_${title}`, data: { path } }); }} diff --git a/app/scripts/modules/core/src/managed/environmentBaseElements/BaseEnvironment.less b/app/scripts/modules/core/src/managed/environmentBaseElements/BaseEnvironment.less index a286ba8342f..9715a95df72 100644 --- a/app/scripts/modules/core/src/managed/environmentBaseElements/BaseEnvironment.less +++ b/app/scripts/modules/core/src/managed/environmentBaseElements/BaseEnvironment.less @@ -1,9 +1,6 @@ @borderRadius: 4px; .BaseEnvironment { - &:not(:first-of-type) { - margin-top: 16px; - } width: 100%; border: 1px solid var(--color-cirrus); border-radius: @borderRadius; diff --git a/app/scripts/modules/core/src/managed/environmentBaseElements/EnvironmentsRender.tsx b/app/scripts/modules/core/src/managed/environmentBaseElements/EnvironmentsRender.tsx new file mode 100644 index 00000000000..23a328478bb --- /dev/null +++ b/app/scripts/modules/core/src/managed/environmentBaseElements/EnvironmentsRender.tsx @@ -0,0 +1,83 @@ +import classnames from 'classnames'; +import { reverse } from 'lodash'; +import React from 'react'; +import { atom, useRecoilState } from 'recoil'; + +import { useElementDimensions } from 'core/presentation/hooks/useDimensions.hook'; +import { logger } from 'core/utils'; + +const STORAGE_KEY = 'MD_environmentsDirection'; + +const DIRECTIONS = ['list', 'sideBySide'] as const; +type Direction = typeof DIRECTIONS[number]; + +const isDirection = (value: string | null): value is Direction => { + return Boolean(value && DIRECTIONS.includes(value as Direction)); +}; + +const storedDirection = localStorage.getItem(STORAGE_KEY); + +const environmentsDirectionState = atom({ + key: 'environmentsDisplay', + default: isDirection(storedDirection) ? storedDirection : 'list', +}); + +// The goal of this hook is to store the value in an atom to be shared across the app but also update the local storage +const useEnvironmentDirection = () => { + const [direction, setDirection] = useRecoilState(environmentsDirectionState); + React.useLayoutEffect(() => { + localStorage.setItem(STORAGE_KEY, direction); + }, [direction]); + + return { direction, setDirection }; +}; + +export const EnvironmentsDirectionController = () => { + const { direction, setDirection } = useEnvironmentDirection(); + return ( + + ); +}; + +const MIN_WIDTH_PER_COLUMN = 500; + +interface IEnvironmentsRenderProps { + className?: string; + children: React.ReactElement[]; +} + +export const EnvironmentsRender = ({ className, children }: IEnvironmentsRenderProps) => { + const { direction } = useEnvironmentDirection(); + const ref = React.useRef(null); + const { width } = useElementDimensions({ ref, isActive: direction === 'sideBySide' }); + let numEnvironments = 1; + if (Array.isArray(children)) { + numEnvironments = children.length; + } else { + logger.log({ + level: 'ERROR', + error: new Error('Environments children should be an array'), + action: 'Environments::Render', + }); + } + + const numColumns = Math.min(Math.round(width / MIN_WIDTH_PER_COLUMN), numEnvironments); + + return ( +
+ {direction === 'list' && children} + {direction === 'sideBySide' && width > 0 ? reverse(React.Children.toArray(children)) : null} +
+ ); +}; diff --git a/app/scripts/modules/core/src/managed/overview/EnvironmentsOverview.tsx b/app/scripts/modules/core/src/managed/overview/EnvironmentsOverview.tsx index c4dd065af7f..c3e9943febc 100644 --- a/app/scripts/modules/core/src/managed/overview/EnvironmentsOverview.tsx +++ b/app/scripts/modules/core/src/managed/overview/EnvironmentsOverview.tsx @@ -7,6 +7,7 @@ import { Resource } from './Resource'; import { Artifact } from './artifact/Artifact'; import { ManagementWarning } from '../config/ManagementWarning'; import { BaseEnvironment } from '../environmentBaseElements/BaseEnvironment'; +import { EnvironmentsRender } from '../environmentBaseElements/EnvironmentsRender'; import { useFetchApplicationQuery, useFetchResourceStatusQuery } from '../graphql/graphql-sdk'; import { QueryEnvironment } from './types'; import { OVERVIEW_VERSION_STATUSES } from './utils'; @@ -40,7 +41,11 @@ export const EnvironmentsOverview = () => {
{environments.length ? ( - environments.map((env) => ) + + {environments.map((env) => ( + + ))} + ) : (
No environments found
)} diff --git a/app/scripts/modules/core/src/managed/overview/artifact/ArtifactVersionTasks.less b/app/scripts/modules/core/src/managed/overview/artifact/ArtifactVersionTasks.less index 459461aa10a..f33f1bcb29e 100644 --- a/app/scripts/modules/core/src/managed/overview/artifact/ArtifactVersionTasks.less +++ b/app/scripts/modules/core/src/managed/overview/artifact/ArtifactVersionTasks.less @@ -10,7 +10,8 @@ & > i { font-size: 14px; - margin-top: 1px; + margin-top: 4px; + align-self: flex-start; } .task-metadata { diff --git a/app/scripts/modules/core/src/managed/versionsHistory/VersionContent.tsx b/app/scripts/modules/core/src/managed/versionsHistory/VersionContent.tsx index 6d71de93323..221e504cac6 100644 --- a/app/scripts/modules/core/src/managed/versionsHistory/VersionContent.tsx +++ b/app/scripts/modules/core/src/managed/versionsHistory/VersionContent.tsx @@ -5,11 +5,12 @@ import { useApplicationContextSafe } from 'core/presentation'; import { BaseEnvironment } from '../environmentBaseElements/BaseEnvironment'; import { EnvironmentItem } from '../environmentBaseElements/EnvironmentItem'; +import { EnvironmentsRender } from '../environmentBaseElements/EnvironmentsRender'; import { useFetchVersionQuery } from '../graphql/graphql-sdk'; import { ArtifactVersionTasks } from '../overview/artifact/ArtifactVersionTasks'; import { Constraints } from '../overview/artifact/Constraints'; import { useCreateVersionActions } from '../overview/artifact/utils'; -import { PinnedVersions, VersionData, VersionInEnvironment } from './types'; +import { HistoryArtifactVersionExtended, PinnedVersions, VersionData } from './types'; import { toPinnedMetadata, VersionMessageData } from '../versionMetadata/MetadataComponents'; import { getBaseMetadata, VersionMetadata } from '../versionMetadata/VersionMetadata'; @@ -17,7 +18,7 @@ import './VersionsHistory.less'; interface IVersionInEnvironmentProps { environment: string; - version: VersionInEnvironment; + version: HistoryArtifactVersionExtended; envPinnedVersions?: PinnedVersions[keyof PinnedVersions]; } @@ -95,7 +96,7 @@ interface IVersionContentProps { export const VersionContent = ({ versionData, pinnedVersions }: IVersionContentProps) => { return ( - + {Object.entries(versionData.environments).map(([env, { versions }]) => { return ( @@ -110,6 +111,6 @@ export const VersionContent = ({ versionData, pinnedVersions }: IVersionContentP ); })} - + ); }; diff --git a/app/scripts/modules/core/src/managed/versionsHistory/types.ts b/app/scripts/modules/core/src/managed/versionsHistory/types.ts index b3879e03bc0..464f750f1a9 100644 --- a/app/scripts/modules/core/src/managed/versionsHistory/types.ts +++ b/app/scripts/modules/core/src/managed/versionsHistory/types.ts @@ -9,7 +9,7 @@ export type SingleVersionEnvironment = NonNullable[number]; export type SingleVersionArtifactVersion = NonNullable[number]; -export interface VersionInEnvironment extends HistoryArtifactVersion { +export interface HistoryArtifactVersionExtended extends HistoryArtifactVersion { reference: string; type: string; } @@ -21,7 +21,7 @@ export interface VersionData { createdAt?: DateTime; isBaking?: boolean; isFocused?: boolean; - environments: { [env: string]: { versions: VersionInEnvironment[]; isPinned?: boolean } }; + environments: { [env: string]: { versions: HistoryArtifactVersionExtended[]; isPinned?: boolean } }; gitMetadata?: HistoryArtifactVersion['gitMetadata']; key: string; } diff --git a/app/scripts/modules/core/src/presentation/hooks/useDimensions.hook.ts b/app/scripts/modules/core/src/presentation/hooks/useDimensions.hook.ts new file mode 100644 index 00000000000..df5216095f3 --- /dev/null +++ b/app/scripts/modules/core/src/presentation/hooks/useDimensions.hook.ts @@ -0,0 +1,33 @@ +import { debounce } from 'lodash'; +import React from 'react'; + +const getElementDimensions = (ref: React.RefObject) => + ref.current ? { width: ref.current.offsetWidth, height: ref.current.offsetHeight } : { width: 0, height: 0 }; + +export const useElementDimensions = ({ + ref, + delay = 200, + isActive = true, +}: { + ref: React.RefObject; + delay?: number; + isActive?: boolean; +}) => { + const [dimension, setDimension] = React.useState(getElementDimensions(ref)); + + React.useLayoutEffect(() => { + const debouncedResizeHandler = debounce(() => { + setDimension(getElementDimensions(ref)); + }, delay); + + if (isActive && ref.current) { + const observer = new ResizeObserver(debouncedResizeHandler); + observer.observe(ref.current); + return () => observer.disconnect(); + } else { + return () => {}; + } + }, [delay, isActive, ref.current]); + + return dimension; +}; diff --git a/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.less b/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.less index 5a2b9aabe86..ef35ef50dcb 100644 --- a/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.less +++ b/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.less @@ -39,4 +39,9 @@ border-bottom: @tab-border-width solid var(--color-accent); color: var(--color-black); } + + .right-element { + margin-left: auto; + align-self: center; + } } diff --git a/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.tsx b/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.tsx index 51aa9eb8c47..31ade7bd8fa 100644 --- a/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.tsx +++ b/app/scripts/modules/core/src/presentation/horizontalTabs/HorizontalTabs.tsx @@ -10,16 +10,17 @@ interface ITabProps { export interface IHorizontalTabsProps { tabs: ITabProps[]; className?: string; - style?: React.CSSProperties; onClick?: (props: ITabProps) => void; + rightElement?: React.ReactElement; } -export const HorizontalTabs = ({ tabs, className, style, onClick }: IHorizontalTabsProps) => { +export const HorizontalTabs = ({ tabs, className, onClick, rightElement }: IHorizontalTabsProps) => { return ( -
+
{tabs.map((tab) => ( ))} + {rightElement &&
{rightElement}
}
); }; diff --git a/package.json b/package.json index 1627733e7f5..b7052f80557 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "react-virtualized": "9.18.5", "react-virtualized-select": "^3.1.3", "react2angular": "^3.2.1", - "recoil": "^0.0.10", + "recoil": "^0.3.1", "reflect-metadata": "^0.1.9", "rxjs": "6.6.7", "rxjs-compat": "6.6.7", @@ -157,6 +157,7 @@ "@types/react-virtualized": "9.7.12", "@types/react-virtualized-select": "^3.0.4", "@types/request-promise-native": "^1.0.15", + "@types/resize-observer-browser": "^0.1.5", "@types/tether": "^1.4.4", "@types/webpack": "4.4.24", "@types/webpack-env": "1.13.7", diff --git a/yarn.lock b/yarn.lock index b866bd3a983..a8cbba699e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4615,6 +4615,11 @@ "@types/node" "*" "@types/tough-cookie" "*" +"@types/resize-observer-browser@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23" + integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -10869,6 +10874,11 @@ gzip-size@^3.0.0: dependencies: duplexer "^0.1.1" +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE= + handle-thing@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" @@ -16745,6 +16755,13 @@ recoil@^0.0.10: resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.0.10.tgz#679ab22306f559f8a63c46fd5ff5241539f9248f" integrity sha512-+9gRqehw3yKETmoZbhSnWu4GO10HDb5xYf1CjLF1oXGK2uT6GX5Lu9mfTXwjxV/jXxEKx8MIRUUbgPxvbJ8SEw== +recoil@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.3.1.tgz#40ef544160d19d76e25de8929d7e512eace13b90" + integrity sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA== + dependencies: + hamt_plus "1.0.2" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"