Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions web/packages/design/src/DraggableList/DraggableList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Copyright 2023 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React, {
Children,
PropsWithChildren,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import styled from 'styled-components';

import { animated, to, useSprings } from '@react-spring/web';
import { useGesture } from '@use-gesture/react';
import { ReactDOMAttributes } from '@use-gesture/react/dist/declarations/src/types';

const Container = styled.div`
position: relative;
`;

interface DraggableListProps {
onOrderChange: (newOrder: number[]) => void;
}

export function DraggableList(props: PropsWithChildren<DraggableListProps>) {
const children = Children.toArray(props.children);

const [childrenHeight, setChildrenHeight] = useState(0);

const animatedDiv = useRef<HTMLDivElement>(null);
const order = useRef<number[]>(children.map((_, index) => index));

function createPropsGetter(
orderList: number[] = order.current,
down?: boolean,
originalIndex?: number,
newHeight?: number
) {
return function getProps(index: number) {
if (down && index === originalIndex) {
return {
height: newHeight,
width: 0,
zIndex: 1,
immediate: (key: string) =>
key === 'active' || key === 'height' || key === 'zIndex',
};
}

return {
height: childrenHeight * orderList.indexOf(index),
width: 0,
zIndex: 0,
immediate: false,
};
};
}

const [springs, api] = useSprings(children.length, createPropsGetter());

const bind = useGesture({
onDrag: state => {
const [, offset] = state.movement;
if (offset === 0) {
return;
}

const [originalIndex] = state.args;

const curIndex = order.current.indexOf(originalIndex);
const newHeight = childrenHeight * curIndex + offset;
const nextIndex = clamp(
Math.round(newHeight / childrenHeight),
0,
children.length - 1
);
const newOrder = swap(order.current, curIndex, nextIndex);

api.start(
createPropsGetter(newOrder, state.down, originalIndex, newHeight)
);
if (!state.down) {
order.current = newOrder;
}
},
onDragEnd: () => {
api.start(createPropsGetter());
props.onOrderChange(order.current);
},
}) as unknown as (...args: any[]) => ReactDOMAttributes; // useGesture typings are wrong https://github.com/pmndrs/use-gesture/issues/362

useEffect(() => {
order.current = children.map((_, index) => index);

api.start(createPropsGetter());
}, [children.length]);

useLayoutEffect(() => {
if (!animatedDiv.current) {
return;
}

const height = animatedDiv.current.scrollHeight;
if (childrenHeight !== height) {
setChildrenHeight(height);

return;
}

api.start(createPropsGetter());
}, [api, childrenHeight, children.length]);

return (
<Container
style={{ height: childrenHeight * (springs.length + 1) }}
onClick={e => e.stopPropagation()}
>
{springs.map((spring, index) => {
const { width, height, zIndex } = spring;

return (
<animated.div
ref={animatedDiv}
{...bind(index)}
key={index}
style={{
position: 'absolute',
zIndex,
width: '100%',
transform: to(
[width, height],
(x, y) => `translate3d(${x}px, ${y}px, 0)`
),
touchAction: 'none',
}}
>
{children[index]}
</animated.div>
);
})}
</Container>
);
}

function clamp(pos: number, low: number, high: number) {
const mid = Math.max(pos, low);

return Math.min(mid, high);
}

function swap<T>(arr: T[], a: number, b: number): T[] {
const copy = [...arr];
const [index] = copy.splice(a, 1);

copy.splice(b, 0, index);

return copy;
}
17 changes: 17 additions & 0 deletions web/packages/design/src/DraggableList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright 2023 Gravitational, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { DraggableList } from './DraggableList';
29 changes: 29 additions & 0 deletions web/packages/design/src/SVGIcon/ConversationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import { SVGIcon } from './SVGIcon';

import type { SVGIconProps } from './common';

export function ConversationListIcon({ size = 24, fill }: SVGIconProps) {
return (
<SVGIcon fill={fill} size={size} viewBox="0 0 24 24">
<path d="M18 8.016V6H6v2.016h12zm-3.984 6V12H6v2.016h8.016zM6 9v2.016h12V9H6zm14.016-6.984q.797 0 1.383.586t.586 1.383v12q0 .797-.586 1.406T20.016 18H6l-3.984 3.984v-18q0-.797.586-1.383t1.383-.586h16.031z" />
</SVGIcon>
);
}
29 changes: 29 additions & 0 deletions web/packages/design/src/SVGIcon/Display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import { SVGIcon } from './SVGIcon';

import type { SVGIconProps } from './common';

export function DisplayIcon({ size = 32, fill }: SVGIconProps) {
return (
<SVGIcon fill={fill} size={size} viewBox="0 0 32 32">
<path d="M30.662 9.003a98.793 98.793 0 0 0-8.815-.851L27 2.999l-2-2-7.017 7.017c-.656-.011-1.317-.017-1.983-.017l-8-8-2 2 6.069 6.069c-3.779.133-7.386.454-10.731.935C.478 12.369 0 16.089 0 20s.477 7.63 1.338 10.997C5.827 31.642 10.786 32 16 32s10.173-.358 14.662-1.003C31.522 27.631 32 23.911 32 20s-.477-7.63-1.338-10.997zm-3.665 18.328C23.63 27.761 19.911 28 16 28s-7.63-.239-10.997-.669C4.358 25.087 4 22.607 4 20s.358-5.087 1.003-7.331C8.369 12.239 12.089 12 16 12s7.63.239 10.996.669C27.641 14.913 28 17.393 28 20s-.358 5.087-1.003 7.331z" />
</SVGIcon>
);
}
29 changes: 29 additions & 0 deletions web/packages/design/src/SVGIcon/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import { SVGIcon } from './SVGIcon';

import type { SVGIconProps } from './common';

export function ErrorIcon({ size = 24, fill }: SVGIconProps) {
return (
<SVGIcon fill={fill} size={size} viewBox="0 0 24 24">
<path d="M12.984 12.984v-6h-1.969v6h1.969zm0 4.032V15h-1.969v2.016h1.969zm-.984-15q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055T12 21.986t-7.055-2.93-2.93-7.055 2.93-7.055T12 2.016z" />
</SVGIcon>
);
}
29 changes: 29 additions & 0 deletions web/packages/design/src/SVGIcon/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import { SVGIcon } from './SVGIcon';

import type { SVGIconProps } from './common';

export function SettingsIcon({ size = 32, fill }: SVGIconProps) {
return (
<SVGIcon fill={fill} size={size} viewBox="0 0 32 32">
<path d="M29.181 19.07c-1.679-2.908-.669-6.634 2.255-8.328l-3.145-5.447a6.022 6.022 0 0 1-3.058.829c-3.361 0-6.085-2.742-6.085-6.125h-6.289a6.023 6.023 0 0 1-.811 3.07C10.369 5.977 6.637 6.966 3.709 5.28L.565 10.727a6.023 6.023 0 0 1 2.246 2.234c1.676 2.903.672 6.623-2.241 8.319l3.145 5.447a6.022 6.022 0 0 1 3.044-.82c3.35 0 6.067 2.725 6.084 6.092h6.289a6.032 6.032 0 0 1 .811-3.038c1.676-2.903 5.399-3.894 8.325-2.219l3.145-5.447a6.032 6.032 0 0 1-2.232-2.226zM16 22.479A6.48 6.48 0 1 1 16 9.52a6.48 6.48 0 0 1 0 12.959z" />
</SVGIcon>
);
}
29 changes: 29 additions & 0 deletions web/packages/design/src/SVGIcon/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';

import { SVGIcon } from './SVGIcon';

import type { SVGIconProps } from './common';

export function TerminalIcon({ size = 32, fill }: SVGIconProps) {
return (
<SVGIcon fill={fill} size={size} viewBox="0 0 32 32">
<path d="M0 2v28h32V2H0zm30 26H2V4h28v24zM28 6H4v20h24V6zM14 16h-2v2h-2v2H8v-2h2v-2h2v-2h-2v-2H8v-2h2v2h2v2h2v2zm8 4h-6v-2h6v2z" />
</SVGIcon>
);
}
5 changes: 5 additions & 0 deletions web/packages/design/src/SVGIcon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ export { CheckIcon } from './Check';
export { ChevronDownIcon } from './ChevronDown';
export { ChevronRightIcon } from './ChevronRight';
export { CloseIcon } from './Close';
export { ConversationListIcon } from './ConversationList';
export { DatabasesIcon } from './Databases';
export { DesktopsIcon } from './Desktops';
export { DevicesIcon } from './Devices';
export { DisplayIcon } from './Display';
export { DownloadsIcon } from './Downloads';
export { EditIcon } from './Edit';
export { ErrorIcon } from './Error';
export { ExpandIcon } from './Expand';
export { ExternalLinkIcon } from './ExternalLink';
export { IntegrationsIcon } from './Integrations';
Expand All @@ -50,8 +53,10 @@ export { SearchIcon } from './Search';
export { ServerIcon } from './Server';
export { ServersIcon } from './Servers';
export { SessionRecordingsIcon } from './SessionRecordings';
export { SettingsIcon } from './Settings';
export { SidebarIcon } from './Sidebar';
export { SupportIcon } from './Support';
export { TerminalIcon } from './Terminal';
export { TrustedClustersIcon } from './TrustedClusters';
export { UpgradeIcon } from './Upgrade';
export { UserIcon } from './User';
Expand Down
2 changes: 2 additions & 0 deletions web/packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"directory": "packages/shared"
},
"dependencies": {
"@react-spring/web": "^9.7.2",
"@use-gesture/react": "^10.2.27",
"ace-builds": "1.4.6",
"create-react-class": "^15.6.3",
"cross-env": "5.0.5",
Expand Down
Loading